aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/Application.cpp43
-rw-r--r--launcher/Application.h9
-rw-r--r--launcher/BaseInstance.cpp21
-rw-r--r--launcher/BaseInstance.h24
-rw-r--r--launcher/CMakeLists.txt84
-rw-r--r--launcher/FileSystem.cpp228
-rw-r--r--launcher/FileSystem.h51
-rw-r--r--launcher/FileSystem_test.cpp122
-rw-r--r--launcher/GZip_test.cpp56
-rw-r--r--launcher/InstanceImportTask.cpp9
-rw-r--r--launcher/InstanceImportTask.h6
-rw-r--r--launcher/InstanceList.cpp428
-rw-r--r--launcher/InstanceList.h13
-rw-r--r--launcher/InstancePageProvider.h6
-rw-r--r--launcher/LaunchController.cpp20
-rw-r--r--launcher/LoggedProcess.cpp32
-rw-r--r--launcher/LoggedProcess.h5
-rw-r--r--launcher/MMCZip.cpp22
-rw-r--r--launcher/MMCZip.h8
-rw-r--r--launcher/NullInstance.h6
-rw-r--r--launcher/QObjectPtr.h90
-rw-r--r--launcher/Version_test.cpp85
-rw-r--r--launcher/java/JavaUtils.cpp12
-rw-r--r--launcher/java/JavaVersion_test.cpp115
-rw-r--r--launcher/meta/Index_test.cpp36
-rw-r--r--launcher/meta/VersionList.cpp7
-rw-r--r--launcher/meta/VersionList.h1
-rw-r--r--launcher/minecraft/GradleSpecifier_test.cpp77
-rw-r--r--launcher/minecraft/Library.cpp3
-rw-r--r--launcher/minecraft/Library_test.cpp270
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp148
-rw-r--r--launcher/minecraft/MinecraftInstance.h30
-rw-r--r--launcher/minecraft/MinecraftUpdate.cpp10
-rw-r--r--launcher/minecraft/MinecraftUpdate.h2
-rw-r--r--launcher/minecraft/MojangVersionFormat_test.cpp53
-rw-r--r--launcher/minecraft/ParseUtils_test.cpp43
-rw-r--r--launcher/minecraft/World.cpp8
-rw-r--r--launcher/minecraft/World.h6
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp2
-rw-r--r--launcher/minecraft/launch/DirectJavaLaunch.cpp4
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp2
-rw-r--r--launcher/minecraft/mod/Mod.cpp180
-rw-r--r--launcher/minecraft/mod/Mod.h70
-rw-r--r--launcher/minecraft/mod/ModDetails.h61
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp617
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h100
-rw-r--r--launcher/minecraft/mod/ModFolderModel_test.cpp92
-rw-r--r--launcher/minecraft/mod/Resource.cpp147
-rw-r--r--launcher/minecraft/mod/Resource.h117
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp524
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h332
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp116
-rw-r--r--launcher/minecraft/mod/ResourcePack.h69
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp187
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h26
-rw-r--r--launcher/minecraft/mod/ShaderPackFolderModel.h10
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp22
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.h6
-rw-r--r--launcher/minecraft/mod/tasks/BasicFolderLoadTask.h64
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp140
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.h22
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp164
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h56
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp26
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.h23
-rw-r--r--launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt1
-rw-r--r--launcher/minecraft/mod/testdata/test_folder/pack.mcmeta6
-rw-r--r--launcher/minecraft/mod/testdata/test_folder/pack.nfo1
-rw-r--r--launcher/minecraft/testdata/1.9-simple.json198
-rw-r--r--launcher/minecraft/testdata/1.9.json529
-rw-r--r--launcher/minecraft/testdata/codecwav-20101023.jar1
-rw-r--r--launcher/minecraft/testdata/lib-native-arch.json46
-rw-r--r--launcher/minecraft/testdata/lib-native.json52
-rw-r--r--launcher/minecraft/testdata/lib-simple.json11
-rw-r--r--launcher/minecraft/testdata/testname-testversion-linux-32.jar1
-rw-r--r--launcher/minecraft/update/FMLLibrariesTask.cpp3
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp124
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h21
-rw-r--r--launcher/modplatform/ModAPI.h4
-rw-r--r--launcher/modplatform/ModIndex.cpp36
-rw-r--r--launcher/modplatform/ModIndex.h6
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp144
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h16
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp64
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h23
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp7
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h3
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp37
-rw-r--r--launcher/modplatform/flame/FlameAPI.h1
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp19
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h3
-rw-r--r--launcher/modplatform/helpers/HashUtils.cpp81
-rw-r--r--launcher/modplatform/helpers/HashUtils.h47
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp20
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h4
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h6
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp282
-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/ModrinthCheckUpdate.cpp39
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp2
-rw-r--r--launcher/modplatform/packwiz/Packwiz_test.cpp87
-rw-r--r--launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml13
-rw-r--r--launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml13
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.h6
-rw-r--r--launcher/mojang/PackageManifest_test.cpp343
-rw-r--r--launcher/mojang/testdata/1.8.0_202-x64.json1
-rwxr-xr-xlauncher/mojang/testdata/inspect/a/b.txt0
l---------launcher/mojang/testdata/inspect/a/b/b.txt1
-rw-r--r--launcher/mojang/testdata/inspect_win/a/b.txt0
-rw-r--r--launcher/mojang/testdata/inspect_win/a/b/b.txt0
-rw-r--r--launcher/net/Download.cpp4
-rw-r--r--launcher/net/Download.h2
-rw-r--r--launcher/net/HttpMetaCache.cpp23
-rw-r--r--launcher/net/HttpMetaCache.h17
-rw-r--r--launcher/net/MetaCacheSink.cpp45
-rw-r--r--launcher/net/MetaCacheSink.h3
-rw-r--r--launcher/net/NetAction.h2
-rw-r--r--launcher/net/NetJob.cpp207
-rw-r--r--launcher/net/NetJob.h48
-rw-r--r--launcher/net/Upload.cpp2
-rw-r--r--launcher/settings/INIFile_test.cpp62
-rw-r--r--launcher/settings/SettingsObject.h1
-rw-r--r--launcher/tasks/ConcurrentTask.cpp39
-rw-r--r--launcher/tasks/ConcurrentTask.h4
-rw-r--r--launcher/tasks/MultipleOptionsTask.cpp41
-rw-r--r--launcher/tasks/MultipleOptionsTask.h14
-rw-r--r--launcher/tasks/SequentialTask.cpp102
-rw-r--r--launcher/tasks/SequentialTask.h60
-rw-r--r--launcher/tasks/Task.cpp24
-rw-r--r--launcher/tasks/Task.h12
-rw-r--r--launcher/tasks/Task_test.cpp67
-rwxr-xr-xlauncher/testdata/FileSystem-test_createShortcut-unix6
-rw-r--r--launcher/testdata/test_folder/assets/minecraft/textures/blah.txt1
-rw-r--r--launcher/testdata/test_folder/pack.mcmeta6
-rw-r--r--launcher/testdata/test_folder/pack.nfo1
-rw-r--r--launcher/ui/MainWindow.cpp35
-rw-r--r--launcher/ui/MainWindow.h1
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp11
-rw-r--r--launcher/ui/dialogs/AboutDialog.ui24
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp28
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.h22
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.ui84
-rw-r--r--launcher/ui/dialogs/LoginDialog.cpp2
-rw-r--r--launcher/ui/dialogs/MSALoginDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp77
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h25
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp17
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h4
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.cpp2
-rw-r--r--launcher/ui/dialogs/OfflineLoginDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp4
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp5
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp2
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp10
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp142
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h13
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.ui6
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp14
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp95
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h17
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h17
-rw-r--r--launcher/ui/pages/instance/ShaderPackPage.h6
-rw-r--r--launcher/ui/pages/instance/TexturePackPage.h6
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp6
-rw-r--r--launcher/ui/pages/instance/VersionPage.ui6
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp12
-rw-r--r--launcher/ui/pages/instance/WorldListPage.h2
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp90
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h13
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp103
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h28
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp59
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.h6
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp95
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h56
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h1
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.h7
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.cpp3
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.ui18
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h7
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp5
-rw-r--r--launcher/ui/widgets/Common.cpp22
-rw-r--r--launcher/ui/widgets/Common.h9
-rw-r--r--launcher/ui/widgets/InfoFrame.cpp261
-rw-r--r--launcher/ui/widgets/InfoFrame.h (renamed from launcher/ui/widgets/MCModInfoFrame.h)38
-rw-r--r--launcher/ui/widgets/InfoFrame.ui (renamed from launcher/ui/widgets/MCModInfoFrame.ui)42
-rw-r--r--launcher/ui/widgets/MCModInfoFrame.cpp168
-rw-r--r--launcher/ui/widgets/ModFilterWidget.cpp65
-rw-r--r--launcher/ui/widgets/ModFilterWidget.h16
-rw-r--r--launcher/ui/widgets/PageContainer.cpp9
-rw-r--r--launcher/ui/widgets/PageContainer.h4
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp94
-rw-r--r--launcher/ui/widgets/ProgressWidget.h48
-rw-r--r--launcher/ui/widgets/ProjectItem.cpp78
-rw-r--r--launcher/ui/widgets/ProjectItem.h25
-rw-r--r--launcher/updater/UpdateChecker.cpp3
-rw-r--r--launcher/updater/UpdateChecker.h2
200 files changed, 5426 insertions, 5291 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 6e25b774..c4179b49 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -117,6 +117,11 @@
#include <sys.h>
+#ifdef Q_OS_LINUX
+#include <dlfcn.h>
+#include "gamemode_client.h"
+#endif
+
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
@@ -325,7 +330,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)
@@ -684,6 +689,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("UpdateDialogGeometry", "");
+ m_settings->registerSetting("ModDownloadGeometry", "");
+
// HACK: This code feels so stupid is there a less stupid way of doing this?
{
m_settings->registerSetting("PastebinURL", "");
@@ -778,7 +785,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM);
auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json";
qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl;
- m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD));
+ m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL));
qDebug() << "<> Updater started.";
}
@@ -868,6 +875,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
+ m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath());
m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath());
@@ -923,6 +931,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
return;
}
+
+ updateCapabilities();
performMainStartupAction();
}
@@ -1271,6 +1281,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;
@@ -1584,14 +1597,30 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex()
return m_metadataIndex;
}
-Application::Capabilities Application::currentCapabilities()
+void Application::updateCapabilities()
{
- Capabilities c;
+ m_capabilities = None;
if (!getMSAClientID().isEmpty())
- c |= SupportsMSA;
+ m_capabilities |= SupportsMSA;
if (!getFlameAPIKey().isEmpty())
- c |= SupportsFlame;
- return c;
+ m_capabilities |= SupportsFlame;
+
+#ifdef Q_OS_LINUX
+ if (gamemode_query_status() >= 0)
+ m_capabilities |= SupportsGameMode;
+
+ {
+ void *dummy = dlopen("libMangoHud_dlsym.so", RTLD_LAZY);
+ // try normal variant as well
+ if (dummy == NULL)
+ dummy = dlopen("libMangoHud.so", RTLD_LAZY);
+
+ if (dummy != NULL) {
+ dlclose(dummy);
+ m_capabilities |= SupportsMangoHud;
+ }
+ }
+#endif
}
QString Application::getJarPath(QString jarFile)
diff --git a/launcher/Application.h b/launcher/Application.h
index 019c3c3d..41fd4c47 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -95,6 +95,8 @@ public:
SupportsMSA = 1 << 0,
SupportsFlame = 1 << 1,
+ SupportsGameMode = 1 << 2,
+ SupportsMangoHud = 1 << 3,
};
Q_DECLARE_FLAGS(Capabilities, Capability)
@@ -162,7 +164,7 @@ public:
shared_qobject_ptr<Meta::Index> metadataIndex();
- Capabilities currentCapabilities();
+ void updateCapabilities();
/*!
* Finds and returns the full path to a jar file.
@@ -180,6 +182,10 @@ public:
return m_rootPath;
}
+ const Capabilities capabilities() {
+ return m_capabilities;
+ }
+
/*!
* Opens a json file using either a system default editor, or, if not empty, the editor
* specified in the settings
@@ -258,6 +264,7 @@ private:
QString m_rootPath;
Status m_status = Application::StartingUp;
+ Capabilities m_capabilities;
#ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
index 5a84a931..e6d4d8e3 100644
--- a/launcher/BaseInstance.cpp
+++ b/launcher/BaseInstance.cpp
@@ -53,15 +53,22 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
: QObject()
{
m_settings = settings;
+ m_global_settings = globalSettings;
m_rootDir = rootDir;
m_settings->registerSetting("name", "Unnamed Instance");
m_settings->registerSetting("iconKey", "default");
m_settings->registerSetting("notes", "");
+
m_settings->registerSetting("lastLaunchTime", 0);
m_settings->registerSetting("totalTimePlayed", 0);
m_settings->registerSetting("lastTimePlayed", 0);
+ // Game time override
+ auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
+ m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
+ m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride);
+
// NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of
// a locally stored instance
if (!m_settings->getSetting("InstanceType"))
@@ -149,7 +156,7 @@ void BaseInstance::setManagedPack(const QString& type, const QString& id, const
int BaseInstance::getConsoleMaxLines() const
{
- auto lineSetting = settings()->getSetting("ConsoleMaxLines");
+ auto lineSetting = m_settings->getSetting("ConsoleMaxLines");
bool conversionOk = false;
int maxLines = lineSetting->get().toInt(&conversionOk);
if(!conversionOk)
@@ -162,7 +169,7 @@ int BaseInstance::getConsoleMaxLines() const
bool BaseInstance::shouldStopOnConsoleOverflow() const
{
- return settings()->get("ConsoleOverflowStop").toBool();
+ return m_settings->get("ConsoleOverflowStop").toBool();
}
void BaseInstance::iconUpdated(QString key)
@@ -237,7 +244,7 @@ void BaseInstance::setRunning(bool running)
int64_t BaseInstance::totalTimePlayed() const
{
- qint64 current = settings()->get("totalTimePlayed").toLongLong();
+ qint64 current = m_settings->get("totalTimePlayed").toLongLong();
if(m_isRunning)
{
QDateTime timeNow = QDateTime::currentDateTime();
@@ -253,7 +260,7 @@ int64_t BaseInstance::lastTimePlayed() const
QDateTime timeNow = QDateTime::currentDateTime();
return m_timeStarted.secsTo(timeNow);
}
- return settings()->get("lastTimePlayed").toLongLong();
+ return m_settings->get("lastTimePlayed").toLongLong();
}
void BaseInstance::resetTimePlayed()
@@ -272,8 +279,10 @@ QString BaseInstance::instanceRoot() const
return m_rootDir;
}
-SettingsObjectPtr BaseInstance::settings() const
+SettingsObjectPtr BaseInstance::settings()
{
+ loadSpecificSettings();
+
return m_settings;
}
@@ -340,7 +349,7 @@ QString BaseInstance::windowTitle() const
}
// FIXME: why is this here? move it to MinecraftInstance!!!
-QStringList BaseInstance::extraArguments() const
+QStringList BaseInstance::extraArguments()
{
return Commandline::splitArgs(settings()->get("JvmArgs").toString());
}
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index 2a94dcc6..3af104e9 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -154,7 +154,7 @@ public:
return level;
};
- virtual QStringList extraArguments() const;
+ virtual QStringList extraArguments();
/// Traits. Normally inside the version, depends on instance implementation.
virtual QSet <QString> traits() const = 0;
@@ -170,9 +170,18 @@ public:
/*!
* \brief Gets this instance's settings object.
* This settings object stores instance-specific settings.
+ *
+ * Note that this method is not const.
+ * It may call loadSpecificSettings() to ensure those are loaded.
+ *
* \return A pointer to this instance's settings object.
*/
- virtual SettingsObjectPtr settings() const;
+ virtual SettingsObjectPtr settings();
+
+ /*!
+ * \brief Loads settings specific to an instance type if they're not already loaded.
+ */
+ virtual void loadSpecificSettings() = 0;
/// returns a valid update task
virtual Task::Ptr createUpdateTask(Net::Mode mode) = 0;
@@ -206,7 +215,7 @@ public:
virtual QString instanceConfigFolder() const = 0;
/// get variables this instance exports
- virtual QMap<QString, QString> getVariables() const = 0;
+ virtual QMap<QString, QString> getVariables() = 0;
virtual QString typeName() const = 0;
@@ -268,6 +277,11 @@ public:
protected:
void changeStatus(Status newStatus);
+ SettingsObjectPtr globalSettings() const { return m_global_settings.lock(); };
+
+ bool isSpecificSettingsLoaded() const { return m_specific_settings_loaded; }
+ void setSpecificSettingsLoaded(bool loaded) { m_specific_settings_loaded = loaded; }
+
signals:
/*!
* \brief Signal emitted when properties relevant to the instance view change
@@ -296,6 +310,10 @@ private: /* data */
bool m_crashed = false;
bool m_hasUpdate = false;
bool m_hasBrokenVersion = false;
+
+ SettingsObjectWeakPtr m_global_settings;
+ bool m_specific_settings_loaded = false;
+
};
Q_DECLARE_METATYPE(shared_qobject_ptr<BaseInstance>)
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index e7afcfc0..e44b98eb 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -88,12 +88,6 @@ set(CORE_SOURCES
MMCTime.cpp
)
-ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME FileSystem) # TODO: needs testdata
-
-ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME GZip)
-
set(PATHMATCHER_SOURCES
# Path matchers
pathmatcher/FSTreeMatcher.h
@@ -318,16 +312,26 @@ set(MINECRAFT_SOURCES
minecraft/mod/ModDetails.h
minecraft/mod/ModFolderModel.h
minecraft/mod/ModFolderModel.cpp
+ minecraft/mod/Resource.h
+ minecraft/mod/Resource.cpp
+ minecraft/mod/ResourceFolderModel.h
+ minecraft/mod/ResourceFolderModel.cpp
+ minecraft/mod/ResourcePack.h
+ minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp
+ minecraft/mod/ShaderPackFolderModel.h
+ minecraft/mod/tasks/BasicFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.cpp
minecraft/mod/tasks/LocalModParseTask.h
minecraft/mod/tasks/LocalModParseTask.cpp
minecraft/mod/tasks/LocalModUpdateTask.h
minecraft/mod/tasks/LocalModUpdateTask.cpp
+ minecraft/mod/tasks/LocalResourcePackParseTask.h
+ minecraft/mod/tasks/LocalResourcePackParseTask.cpp
# Assets
minecraft/AssetsUtils.h
@@ -345,42 +349,6 @@ set(MINECRAFT_SOURCES
mojang/PackageManifest.cpp
minecraft/Agent.h)
-ecm_add_test(minecraft/GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME GradleSpecifier)
-
-if(BUILD_TESTING)
- add_executable(PackageManifest
- mojang/PackageManifest_test.cpp
- )
- target_link_libraries(PackageManifest
- Launcher_logic
- Qt${QT_VERSION_MAJOR}::Test
- )
- target_include_directories(PackageManifest
- PRIVATE ../cmake/UnitTest/
- )
- add_test(
- NAME PackageManifest
- COMMAND PackageManifest
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
- )
-endif()
-
-# TODO: needs minecraft/testdata
-ecm_add_test(minecraft/MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME MojangVersionFormat)
-
-ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME Library)
-
-# FIXME: shares data with FileSystem test
-# TODO: needs testdata
-ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME ModFolderModel)
-
-ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME ParseUtils)
-
# the screenshots feature
set(SCREENSHOTS_SOURCES
screenshots/Screenshot.h
@@ -402,9 +370,6 @@ set(TASKS_SOURCES
tasks/MultipleOptionsTask.cpp
)
-ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME Task)
-
set(SETTINGS_SOURCES
# Settings
settings/INIFile.cpp
@@ -421,9 +386,6 @@ set(SETTINGS_SOURCES
settings/SettingsObject.h
)
-ecm_add_test(settings/INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME INIFile)
-
set(JAVA_SOURCES
java/JavaChecker.h
java/JavaChecker.cpp
@@ -439,9 +401,6 @@ set(JAVA_SOURCES
java/JavaVersion.cpp
)
-ecm_add_test(java/JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME JavaVersion)
-
set(TRANSLATIONS_SOURCES
translations/TranslationsModel.h
translations/TranslationsModel.cpp
@@ -494,6 +453,8 @@ set(API_SOURCES
modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h
modplatform/helpers/NetworkModAPI.cpp
+ modplatform/helpers/HashUtils.h
+ modplatform/helpers/HashUtils.cpp
)
set(FTB_SOURCES
@@ -542,9 +503,6 @@ set(PACKWIZ_SOURCES
modplatform/packwiz/Packwiz.cpp
)
-# TODO: needs modplatform/packwiz/testdata
-ecm_add_test(modplatform/packwiz/Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME Packwiz)
set(TECHNIC_SOURCES
modplatform/technic/SingleZipPackInstallTask.h
@@ -568,9 +526,6 @@ set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLShareCode.h
)
-ecm_add_test(meta/Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME Index)
-
################################ COMPILE ################################
# we need zlib
@@ -764,6 +719,8 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
ui/pages/modplatform/atlauncher/AtlPage.cpp
ui/pages/modplatform/atlauncher/AtlPage.h
+ ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
+ ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
ui/pages/modplatform/ftb/FtbFilterModel.cpp
ui/pages/modplatform/ftb/FtbFilterModel.h
@@ -849,6 +806,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ModDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
+ ui/dialogs/BlockedModsDialog.cpp
+ ui/dialogs/BlockedModsDialog.h
ui/dialogs/ChooseProviderDialog.h
ui/dialogs/ChooseProviderDialog.cpp
ui/dialogs/ModUpdateDialog.cpp
@@ -875,8 +834,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/LineSeparator.h
ui/widgets/LogView.cpp
ui/widgets/LogView.h
- ui/widgets/MCModInfoFrame.cpp
- ui/widgets/MCModInfoFrame.h
+ ui/widgets/InfoFrame.cpp
+ ui/widgets/InfoFrame.h
ui/widgets/ModFilterWidget.cpp
ui/widgets/ModFilterWidget.h
ui/widgets/ModListView.cpp
@@ -884,6 +843,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/PageContainer.cpp
ui/widgets/PageContainer.h
ui/widgets/PageContainer_p.h
+ ui/widgets/ProjectItem.h
+ ui/widgets/ProjectItem.cpp
ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp
@@ -948,7 +909,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
- ui/widgets/MCModInfoFrame.ui
+ ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui
@@ -968,6 +929,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
+ ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui
)
@@ -992,13 +954,13 @@ endif()
# Add executable
add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES})
+target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
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..8eeb2885 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
@@ -516,6 +467,7 @@ bool overrideFolder(QString overwritten_path, QString override_path)
for (auto file : listFolderPaths(root_override)) {
QString destination = file;
destination.replace(override_path, overwritten_path);
+ ensureFilePathExists(destination);
qDebug() << QString("Applying override %1 in %2").arg(file, destination);
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/FileSystem_test.cpp b/launcher/FileSystem_test.cpp
deleted file mode 100644
index 99ae9269..00000000
--- a/launcher/FileSystem_test.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-#include <QTest>
-#include <QTemporaryDir>
-#include <QStandardPaths>
-
-#include "FileSystem.h"
-
-class FileSystemTest : public QObject
-{
- Q_OBJECT
-
- const QString bothSlash = "/foo/";
- const QString trailingSlash = "foo/";
- const QString leadingSlash = "/foo";
-
-private
-slots:
- void test_pathCombine()
- {
- QCOMPARE(QString("/foo/foo"), FS::PathCombine(bothSlash, bothSlash));
- QCOMPARE(QString("foo/foo"), FS::PathCombine(trailingSlash, trailingSlash));
- QCOMPARE(QString("/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash));
-
- QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(bothSlash, bothSlash, bothSlash));
- QCOMPARE(QString("foo/foo/foo"), FS::PathCombine(trailingSlash, trailingSlash, trailingSlash));
- QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash, leadingSlash));
- }
-
- void test_PathCombine1_data()
- {
- QTest::addColumn<QString>("result");
- QTest::addColumn<QString>("path1");
- QTest::addColumn<QString>("path2");
-
- QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl";
- QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl";
-#if defined(Q_OS_WIN)
- QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc";
- QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl";
- QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl";
-#endif
- }
-
- void test_PathCombine1()
- {
- QFETCH(QString, result);
- QFETCH(QString, path1);
- QFETCH(QString, path2);
-
- QCOMPARE(FS::PathCombine(path1, path2), result);
- }
-
- void test_PathCombine2_data()
- {
- QTest::addColumn<QString>("result");
- QTest::addColumn<QString>("path1");
- QTest::addColumn<QString>("path2");
- QTest::addColumn<QString>("path3");
-
- QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl";
- QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl";
- QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl";
- QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl";
-#if defined(Q_OS_WIN)
- QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl";
- QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl";
- QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl";
- QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl";
-#endif
- }
-
- void test_PathCombine2()
- {
- QFETCH(QString, result);
- QFETCH(QString, path1);
- QFETCH(QString, path2);
- QFETCH(QString, path3);
-
- QCOMPARE(FS::PathCombine(path1, path2, path3), result);
- }
-
- void test_copy()
- {
- QString folder = QFINDTESTDATA("testdata/test_folder");
- auto f = [&folder]()
- {
- QTemporaryDir tempDir;
- tempDir.setAutoRemove(true);
- qDebug() << "From:" << folder << "To:" << tempDir.path();
-
- QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder"));
- qDebug() << tempDir.path();
- qDebug() << target_dir.path();
- FS::copy c(folder, target_dir.path());
- c();
-
- for(auto entry: target_dir.entryList())
- {
- qDebug() << entry;
- }
- QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
- QVERIFY(target_dir.entryList().contains("assets"));
- };
-
- // first try variant without trailing /
- QVERIFY(!folder.endsWith('/'));
- f();
-
- // then variant with trailing /
- folder.append('/');
- QVERIFY(folder.endsWith('/'));
- f();
- }
-
- void test_getDesktop()
- {
- QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
- }
-};
-
-QTEST_GUILESS_MAIN(FileSystemTest)
-
-#include "FileSystem_test.moc"
diff --git a/launcher/GZip_test.cpp b/launcher/GZip_test.cpp
deleted file mode 100644
index 73859fbc..00000000
--- a/launcher/GZip_test.cpp
+++ /dev/null
@@ -1,56 +0,0 @@
-#include <QTest>
-
-#include "GZip.h"
-#include <random>
-
-void fib(int &prev, int &cur)
-{
- auto ret = prev + cur;
- prev = cur;
- cur = ret;
-}
-
-class GZipTest : public QObject
-{
- Q_OBJECT
-private
-slots:
-
- void test_Through()
- {
- // test up to 10 MB
- static const int size = 10 * 1024 * 1024;
- QByteArray random;
- QByteArray compressed;
- QByteArray decompressed;
- std::default_random_engine eng((std::random_device())());
- std::uniform_int_distribution<uint8_t> idis(0, std::numeric_limits<uint8_t>::max());
-
- // initialize random buffer
- for(int i = 0; i < size; i++)
- {
- random.append((char)idis(eng));
- }
-
- // initialize fibonacci
- int prev = 1;
- int cur = 1;
-
- // test if fibonacci long random buffers pass through GZip
- do
- {
- QByteArray copy = random;
- copy.resize(cur);
- compressed.clear();
- decompressed.clear();
- QVERIFY(GZip::zip(copy, compressed));
- QVERIFY(GZip::unzip(compressed, decompressed));
- QCOMPARE(decompressed, copy);
- fib(prev, cur);
- } while (cur < size);
- }
-};
-
-QTEST_GUILESS_MAIN(GZipTest)
-
-#include "GZip_test.moc"
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 14e1cd47..de0afc96 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -60,7 +60,7 @@
#include "net/ChecksumValidator.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/ScrollMessageBox.h"
+#include "ui/dialogs/BlockedModsDialog.h"
#include <algorithm>
@@ -396,21 +396,24 @@ void InstanceImportTask::processFlame()
auto results = m_modIdResolver->getResults();
//first check for blocked mods
QString text;
+ QList<QUrl> urls;
auto anyBlocked = false;
for(const auto& result: results.files.values()) {
if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
+ urls.append(QUrl(result.websiteUrl));
anyBlocked = true;
}
}
if(anyBlocked) {
qWarning() << "Blocked mods found, displaying mod list";
- auto message_dialog = new ScrollMessageBox(m_parent,
+ auto message_dialog = new BlockedModsDialog(m_parent,
tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"),
- text);
+ text,
+ urls);
message_dialog->setModal(true);
if (message_dialog->exec()) {
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/InstancePageProvider.h b/launcher/InstancePageProvider.h
index 78fb7016..bf29377d 100644
--- a/launcher/InstancePageProvider.h
+++ b/launcher/InstancePageProvider.h
@@ -37,9 +37,9 @@ public:
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
values.append(modsPage);
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
- values.append(new ResourcePackPage(onesix.get()));
- values.append(new TexturePackPage(onesix.get()));
- values.append(new ShaderPackPage(onesix.get()));
+ values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
+ values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
+ values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
values.append(new NotesPage(onesix.get()));
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
values.append(new ServersPage(onesix));
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/LoggedProcess.cpp b/launcher/LoggedProcess.cpp
index fbdeed8f..6447f5c6 100644
--- a/launcher/LoggedProcess.cpp
+++ b/launcher/LoggedProcess.cpp
@@ -34,8 +34,9 @@
*/
#include "LoggedProcess.h"
-#include "MessageLevel.h"
#include <QDebug>
+#include <QTextDecoder>
+#include "MessageLevel.h"
LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
{
@@ -59,25 +60,26 @@ LoggedProcess::~LoggedProcess()
}
}
-QStringList reprocess(const QByteArray & data, QString & leftover)
+QStringList reprocess(const QByteArray& data, QTextDecoder& decoder)
{
- QString str = leftover + QString::fromLocal8Bit(data);
-
- str.remove('\r');
- QStringList lines = str.split("\n");
- leftover = lines.takeLast();
+ auto str = decoder.toUnicode(data);
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts);
+#else
+ auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts);
+#endif
return lines;
}
void LoggedProcess::on_stdErr()
{
- auto lines = reprocess(readAllStandardError(), m_err_leftover);
+ auto lines = reprocess(readAllStandardError(), m_err_decoder);
emit log(lines, MessageLevel::StdErr);
}
void LoggedProcess::on_stdOut()
{
- auto lines = reprocess(readAllStandardOutput(), m_out_leftover);
+ auto lines = reprocess(readAllStandardOutput(), m_out_decoder);
emit log(lines, MessageLevel::StdOut);
}
@@ -86,18 +88,6 @@ void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status)
// save the exit code
m_exit_code = exit_code;
- // Flush console window
- if (!m_err_leftover.isEmpty())
- {
- emit log({m_err_leftover}, MessageLevel::StdErr);
- m_err_leftover.clear();
- }
- if (!m_out_leftover.isEmpty())
- {
- emit log({m_err_leftover}, MessageLevel::StdOut);
- m_out_leftover.clear();
- }
-
// based on state, send signals
if (!m_is_aborting)
{
diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h
index 61e74bd9..2360d1ea 100644
--- a/launcher/LoggedProcess.h
+++ b/launcher/LoggedProcess.h
@@ -36,6 +36,7 @@
#pragma once
#include <QProcess>
+#include <QTextDecoder>
#include "MessageLevel.h"
/*
@@ -88,8 +89,8 @@ private:
void changeState(LoggedProcess::State state);
private:
- QString m_err_leftover;
- QString m_out_leftover;
+ QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
+ QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
bool m_killed = false;
State m_state = NotRunning;
int m_exit_code = 0;
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index 8518e606..9f4e968f 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// do not merge disabled mods.
if (!mod->enabled())
continue;
- if (mod->type() == Mod::MOD_ZIPFILE)
+ if (mod->type() == ResourceType::ZIPFILE)
{
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{
@@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
return false;
}
}
- else if (mod->type() == Mod::MOD_SINGLEFILE)
+ else if (mod->type() == ResourceType::SINGLEFILE)
{
// FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo();
@@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
}
addedFiles.insert(filename.fileName());
}
- else if (mod->type() == Mod::MOD_FOLDER)
+ else if (mod->type() == ResourceType::FOLDER)
{
// untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles
@@ -268,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;
@@ -277,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...";
@@ -286,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
@@ -323,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);
@@ -341,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))
@@ -352,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))
@@ -369,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 7f43d158..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
{
@@ -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/NullInstance.h b/launcher/NullInstance.h
index 9b0a9331..53e64a05 100644
--- a/launcher/NullInstance.h
+++ b/launcher/NullInstance.h
@@ -15,6 +15,10 @@ public:
void saveNow() override
{
}
+ void loadSpecificSettings() override
+ {
+ setSpecificSettingsLoaded(true);
+ }
QString getStatusbarDescription() override
{
return tr("Unknown instance type");
@@ -43,7 +47,7 @@ public:
{
return QProcessEnvironment();
}
- QMap<QString, QString> getVariables() const override
+ QMap<QString, QString> getVariables() override
{
return QMap<QString, QString>();
}
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index 173dc5e7..b1ef1c8d 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -1,91 +1,37 @@
#pragma once
+#include <QObject>
+#include <QSharedPointer>
+
#include <functional>
#include <memory>
-#include <QObject>
-namespace details
-{
-struct DeleteQObjectLater
-{
- void operator()(QObject *obj) const
- {
- obj->deleteLater();
- }
-};
-}
/**
* A unique pointer class with unique pointer semantics intended for derivates of QObject
* Calls deleteLater() instead of destroying the contained object immediately
*/
-template<typename T> using unique_qobject_ptr = std::unique_ptr<T, details::DeleteQObjectLater>;
+template <typename T>
+using unique_qobject_ptr = QScopedPointer<T, QScopedPointerDeleteLater>;
/**
* A shared pointer class with shared pointer semantics intended for derivates of QObject
* Calls deleteLater() instead of destroying the contained object immediately
*/
template <typename T>
-class shared_qobject_ptr
-{
-public:
- shared_qobject_ptr(){}
- shared_qobject_ptr(T * wrap)
- {
- reset(wrap);
- }
- shared_qobject_ptr(const shared_qobject_ptr<T>& other)
- {
- m_ptr = other.m_ptr;
- }
- template<typename Derived>
- shared_qobject_ptr(const shared_qobject_ptr<Derived> &other)
- {
- m_ptr = other.unwrap();
- }
+class shared_qobject_ptr : public QSharedPointer<T> {
+ public:
+ constexpr shared_qobject_ptr() : QSharedPointer<T>() {}
+ constexpr shared_qobject_ptr(T* ptr) : QSharedPointer<T>(ptr, &QObject::deleteLater) {}
+ constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer<T>(null_ptr, &QObject::deleteLater) {}
-public:
- void reset(T * wrap)
- {
- using namespace std::placeholders;
- m_ptr.reset(wrap, std::bind(&QObject::deleteLater, _1));
- }
- void reset(const shared_qobject_ptr<T> &other)
- {
- m_ptr = other.m_ptr;
- }
- void reset()
- {
- m_ptr.reset();
- }
- T * get() const
- {
- return m_ptr.get();
- }
- T * operator->() const
- {
- return m_ptr.get();
- }
- T & operator*() const
- {
- return *m_ptr.get();
- }
- operator bool() const
- {
- return m_ptr.get() != nullptr;
- }
- const std::shared_ptr <T> unwrap() const
+ template <typename Derived>
+ constexpr shared_qobject_ptr(const shared_qobject_ptr<Derived>& other) : QSharedPointer<T>(other)
+ {}
+
+ void reset() { QSharedPointer<T>::reset(); }
+ void reset(const shared_qobject_ptr<T>& other)
{
- return m_ptr;
+ shared_qobject_ptr<T> t(other);
+ this->swap(t);
}
- template<typename U>
- bool operator==(const shared_qobject_ptr<U>& other) const {
- return m_ptr == other.m_ptr;
- }
- template<typename U>
- bool operator!=(const shared_qobject_ptr<U>& other) const {
- return m_ptr != other.m_ptr;
- }
-
-private:
- std::shared_ptr <T> m_ptr;
};
diff --git a/launcher/Version_test.cpp b/launcher/Version_test.cpp
deleted file mode 100644
index b2d657a6..00000000
--- a/launcher/Version_test.cpp
+++ /dev/null
@@ -1,85 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <QTest>
-
-#include "TestUtil.h"
-#include <Version.h>
-
-class ModUtilsTest : public QObject
-{
- Q_OBJECT
- void setupVersions()
- {
- QTest::addColumn<QString>("first");
- QTest::addColumn<QString>("second");
- QTest::addColumn<bool>("lessThan");
- QTest::addColumn<bool>("equal");
-
- QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true;
- QTest::newRow("equal, implicit 1") << "1.2" << "1.2.0" << false << true;
- QTest::newRow("equal, implicit 2") << "1.2.0" << "1.2" << false << true;
- QTest::newRow("equal, two-digit") << "1.42" << "1.42" << false << true;
-
- QTest::newRow("lessThan, explicit 1") << "1.2.0" << "1.2.1" << true << false;
- QTest::newRow("lessThan, explicit 2") << "1.2.0" << "1.3.0" << true << false;
- QTest::newRow("lessThan, explicit 3") << "1.2.0" << "2.2.0" << true << false;
- QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.1" << true << false;
- QTest::newRow("lessThan, implicit 2") << "1.2" << "1.3.0" << true << false;
- QTest::newRow("lessThan, implicit 3") << "1.2" << "2.2.0" << true << false;
- QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false;
-
- QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false;
- QTest::newRow("greaterThan, explicit 2") << "1.3.0" << "1.2.0" << false << false;
- QTest::newRow("greaterThan, explicit 3") << "2.2.0" << "1.2.0" << false << false;
- QTest::newRow("greaterThan, implicit 1") << "1.2.1" << "1.2" << false << false;
- QTest::newRow("greaterThan, implicit 2") << "1.3.0" << "1.2" << false << false;
- QTest::newRow("greaterThan, implicit 3") << "2.2.0" << "1.2" << false << false;
- QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false;
- }
-
-private slots:
- void initTestCase()
- {
-
- }
- void cleanupTestCase()
- {
-
- }
-
- void test_versionCompare_data()
- {
- setupVersions();
- }
- void test_versionCompare()
- {
- QFETCH(QString, first);
- QFETCH(QString, second);
- QFETCH(bool, lessThan);
- QFETCH(bool, equal);
-
- const auto v1 = Version(first);
- const auto v2 = Version(second);
-
- QCOMPARE(v1 < v2, lessThan);
- QCOMPARE(v1 > v2, !lessThan && !equal);
- QCOMPARE(v1 == v2, equal);
- }
-};
-
-QTEST_GUILESS_MAIN(ModUtilsTest)
-
-#include "Version_test.moc"
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 2b19fca0..2f91605b 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -174,11 +174,17 @@ JavaInstallPtr JavaUtils::GetDefaultJava()
QStringList addJavasFromEnv(QList<QString> javas)
{
- QByteArray env = qgetenv("POLYMC_JAVA_PATHS");
+ auto env = qEnvironmentVariable("POLYMC_JAVA_PATHS");
#if defined(Q_OS_WIN32)
- QList<QString> javaPaths = QString::fromLocal8Bit(env).replace("\\", "/").split(QLatin1String(";"));
+ QList<QString> javaPaths = env.replace("\\", "/").split(QLatin1String(";"));
+
+ auto envPath = qEnvironmentVariable("PATH");
+ QList<QString> javaPathsfromPath = envPath.replace("\\", "/").split(QLatin1String(";"));
+ for (QString string : javaPathsfromPath) {
+ javaPaths.append(string + "/javaw.exe");
+ }
#else
- QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":"));
+ QList<QString> javaPaths = env.split(QLatin1String(":"));
#endif
for(QString i : javaPaths)
{
diff --git a/launcher/java/JavaVersion_test.cpp b/launcher/java/JavaVersion_test.cpp
deleted file mode 100644
index 545947ef..00000000
--- a/launcher/java/JavaVersion_test.cpp
+++ /dev/null
@@ -1,115 +0,0 @@
-#include <QTest>
-
-#include "java/JavaVersion.h"
-
-class JavaVersionTest : public QObject
-{
- Q_OBJECT
-private
-slots:
- void test_Parse_data()
- {
- QTest::addColumn<QString>("string");
- QTest::addColumn<int>("major");
- QTest::addColumn<int>("minor");
- QTest::addColumn<int>("security");
- QTest::addColumn<QString>("prerelease");
-
- QTest::newRow("old format") << "1.6.0_33" << 6 << 0 << 33 << QString();
- QTest::newRow("old format prerelease") << "1.9.0_1-ea" << 9 << 0 << 1 << "ea";
-
- QTest::newRow("new format major") << "9" << 9 << 0 << 0 << QString();
- QTest::newRow("new format minor") << "9.1" << 9 << 1 << 0 << QString();
- QTest::newRow("new format security") << "9.0.1" << 9 << 0 << 1 << QString();
- QTest::newRow("new format prerelease") << "9-ea" << 9 << 0 << 0 << "ea";
- QTest::newRow("new format long prerelease") << "9.0.1-ea" << 9 << 0 << 1 << "ea";
- }
- void test_Parse()
- {
- QFETCH(QString, string);
- QFETCH(int, major);
- QFETCH(int, minor);
- QFETCH(int, security);
- QFETCH(QString, prerelease);
-
- JavaVersion test(string);
- QCOMPARE(test.m_string, string);
- QCOMPARE(test.toString(), string);
- QCOMPARE(test.m_major, major);
- QCOMPARE(test.m_minor, minor);
- QCOMPARE(test.m_security, security);
- QCOMPARE(test.m_prerelease, prerelease);
- }
-
- void test_Sort_data()
- {
- QTest::addColumn<QString>("lhs");
- QTest::addColumn<QString>("rhs");
- QTest::addColumn<bool>("smaller");
- QTest::addColumn<bool>("equal");
- QTest::addColumn<bool>("bigger");
-
- // old format and new format equivalence
- QTest::newRow("1.6.0_33 == 6.0.33") << "1.6.0_33" << "6.0.33" << false << true << false;
- // old format major version
- QTest::newRow("1.5.0_33 < 1.6.0_33") << "1.5.0_33" << "1.6.0_33" << true << false << false;
- // new format - first release vs first security patch
- QTest::newRow("9 < 9.0.1") << "9" << "9.0.1" << true << false << false;
- QTest::newRow("9.0.1 > 9") << "9.0.1" << "9" << false << false << true;
- // new format - first minor vs first release/security patch
- QTest::newRow("9.1 > 9.0.1") << "9.1" << "9.0.1" << false << false << true;
- QTest::newRow("9.0.1 < 9.1") << "9.0.1" << "9.1" << true << false << false;
- QTest::newRow("9.1 > 9") << "9.1" << "9" << false << false << true;
- QTest::newRow("9 > 9.1") << "9" << "9.1" << true << false << false;
- // new format - omitted numbers
- QTest::newRow("9 == 9.0") << "9" << "9.0" << false << true << false;
- QTest::newRow("9 == 9.0.0") << "9" << "9.0.0" << false << true << false;
- QTest::newRow("9.0 == 9.0.0") << "9.0" << "9.0.0" << false << true << false;
- // early access and prereleases compared to final release
- QTest::newRow("9-ea < 9") << "9-ea" << "9" << true << false << false;
- QTest::newRow("9 < 9.0.1-ea") << "9" << "9.0.1-ea" << true << false << false;
- QTest::newRow("9.0.1-ea > 9") << "9.0.1-ea" << "9" << false << false << true;
- // prerelease difference only testing
- QTest::newRow("9-1 == 9-1") << "9-1" << "9-1" << false << true << false;
- QTest::newRow("9-1 < 9-2") << "9-1" << "9-2" << true << false << false;
- QTest::newRow("9-5 < 9-20") << "9-5" << "9-20" << true << false << false;
- QTest::newRow("9-rc1 < 9-rc2") << "9-rc1" << "9-rc2" << true << false << false;
- QTest::newRow("9-rc5 < 9-rc20") << "9-rc5" << "9-rc20" << true << false << false;
- QTest::newRow("9-rc < 9-rc2") << "9-rc" << "9-rc2" << true << false << false;
- QTest::newRow("9-ea < 9-rc") << "9-ea" << "9-rc" << true << false << false;
- }
- void test_Sort()
- {
- QFETCH(QString, lhs);
- QFETCH(QString, rhs);
- QFETCH(bool, smaller);
- QFETCH(bool, equal);
- QFETCH(bool, bigger);
- JavaVersion lver(lhs);
- JavaVersion rver(rhs);
- QCOMPARE(lver < rver, smaller);
- QCOMPARE(lver == rver, equal);
- QCOMPARE(lver > rver, bigger);
- }
- void test_PermGen_data()
- {
- QTest::addColumn<QString>("version");
- QTest::addColumn<bool>("needs_permgen");
- QTest::newRow("1.6.0_33") << "1.6.0_33" << true;
- QTest::newRow("1.7.0_60") << "1.7.0_60" << true;
- QTest::newRow("1.8.0_22") << "1.8.0_22" << false;
- QTest::newRow("9-ea") << "9-ea" << false;
- QTest::newRow("9.2.4") << "9.2.4" << false;
- }
- void test_PermGen()
- {
- QFETCH(QString, version);
- QFETCH(bool, needs_permgen);
- JavaVersion v(version);
- QCOMPARE(needs_permgen, v.requiresPermGen());
- }
-};
-
-QTEST_GUILESS_MAIN(JavaVersionTest)
-
-#include "JavaVersion_test.moc"
diff --git a/launcher/meta/Index_test.cpp b/launcher/meta/Index_test.cpp
deleted file mode 100644
index 261858c4..00000000
--- a/launcher/meta/Index_test.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#include <QTest>
-
-#include "meta/Index.h"
-#include "meta/VersionList.h"
-
-class IndexTest : public QObject
-{
- Q_OBJECT
-private
-slots:
- void test_hasUid_and_getList()
- {
- Meta::Index windex({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")});
- QVERIFY(windex.hasUid("list1"));
- QVERIFY(!windex.hasUid("asdf"));
- QVERIFY(windex.get("list2") != nullptr);
- QCOMPARE(windex.get("list2")->uid(), QString("list2"));
- QVERIFY(windex.get("adsf") != nullptr);
- }
-
- void test_merge()
- {
- Meta::Index windex({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")});
- QCOMPARE(windex.lists().size(), 3);
- windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")})));
- QCOMPARE(windex.lists().size(), 3);
- windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list4"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list5")})));
- QCOMPARE(windex.lists().size(), 5);
- windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list6")})));
- QCOMPARE(windex.lists().size(), 6);
- }
-};
-
-QTEST_GUILESS_MAIN(IndexTest)
-
-#include "Index_test.moc"
diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp
index 6d23ce9a..f609e94c 100644
--- a/launcher/meta/VersionList.cpp
+++ b/launcher/meta/VersionList.cpp
@@ -140,6 +140,13 @@ VersionPtr VersionList::getVersion(const QString &version)
return out;
}
+bool VersionList::hasVersion(QString version) const
+{
+ auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(),
+ [&](Meta::VersionPtr const& a){ return a->version() == version; });
+ return (ver != m_versions.constEnd());
+}
+
void VersionList::setName(const QString &name)
{
m_name = name;
diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h
index 378255df..a6db2fd7 100644
--- a/launcher/meta/VersionList.h
+++ b/launcher/meta/VersionList.h
@@ -66,6 +66,7 @@ public:
QString humanReadable() const;
VersionPtr getVersion(const QString &version);
+ bool hasVersion(QString version) const;
QVector<VersionPtr> versions() const
{
diff --git a/launcher/minecraft/GradleSpecifier_test.cpp b/launcher/minecraft/GradleSpecifier_test.cpp
deleted file mode 100644
index a062dfac..00000000
--- a/launcher/minecraft/GradleSpecifier_test.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-#include <QTest>
-
-#include "minecraft/GradleSpecifier.h"
-
-class GradleSpecifierTest : public QObject
-{
- Q_OBJECT
-private
-slots:
- void initTestCase()
- {
-
- }
- void cleanupTestCase()
- {
-
- }
-
- void test_Positive_data()
- {
- QTest::addColumn<QString>("through");
-
- QTest::newRow("3 parter") << "org.gradle.test.classifiers:service:1.0";
- QTest::newRow("classifier") << "org.gradle.test.classifiers:service:1.0:jdk15";
- QTest::newRow("jarextension") << "org.gradle.test.classifiers:service:1.0@jar";
- QTest::newRow("jarboth") << "org.gradle.test.classifiers:service:1.0:jdk15@jar";
- QTest::newRow("packxz") << "org.gradle.test.classifiers:service:1.0:jdk15@jar.pack.xz";
- }
- void test_Positive()
- {
- QFETCH(QString, through);
-
- QString converted = GradleSpecifier(through).serialize();
-
- QCOMPARE(converted, through);
- }
-
- void test_Path_data()
- {
- QTest::addColumn<QString>("spec");
- QTest::addColumn<QString>("expected");
-
- QTest::newRow("3 parter") << "group.id:artifact:1.0" << "group/id/artifact/1.0/artifact-1.0.jar";
- QTest::newRow("doom") << "id.software:doom:1.666:demons@wad" << "id/software/doom/1.666/doom-1.666-demons.wad";
- }
- void test_Path()
- {
- QFETCH(QString, spec);
- QFETCH(QString, expected);
-
- QString converted = GradleSpecifier(spec).toPath();
-
- QCOMPARE(converted, expected);
- }
- void test_Negative_data()
- {
- QTest::addColumn<QString>("input");
-
- QTest::newRow("too many :") << "org:gradle.test:class:::ifiers:service:1.0::";
- QTest::newRow("nonsense") << "I like turtles";
- QTest::newRow("empty string") << "";
- QTest::newRow("missing version") << "herp.derp:artifact";
- }
- void test_Negative()
- {
- QFETCH(QString, input);
-
- GradleSpecifier spec(input);
- QVERIFY(!spec.valid());
- QCOMPARE(spec.serialize(), input);
- QCOMPARE(spec.toPath(), QString());
- }
-};
-
-QTEST_GUILESS_MAIN(GradleSpecifierTest)
-
-#include "GradleSpecifier_test.moc"
diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp
index c7982705..ba7aed4b 100644
--- a/launcher/minecraft/Library.cpp
+++ b/launcher/minecraft/Library.cpp
@@ -88,6 +88,9 @@ QList<NetAction::Ptr> Library::getDownloads(
options |= Net::Download::Option::AcceptLocalFiles;
}
+ // Don't add a time limit for the libraries cache entry validity
+ options |= Net::Download::Option::MakeEternal;
+
if(sha1.size())
{
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
diff --git a/launcher/minecraft/Library_test.cpp b/launcher/minecraft/Library_test.cpp
deleted file mode 100644
index 834dd558..00000000
--- a/launcher/minecraft/Library_test.cpp
+++ /dev/null
@@ -1,270 +0,0 @@
-#include <QTest>
-
-#include "minecraft/MojangVersionFormat.h"
-#include "minecraft/OneSixVersionFormat.h"
-#include "minecraft/Library.h"
-#include "net/HttpMetaCache.h"
-#include "FileSystem.h"
-
-class LibraryTest : public QObject
-{
- Q_OBJECT
-private:
- LibraryPtr readMojangJson(const QString path)
- {
- QFile jsonFile(path);
- jsonFile.open(QIODevice::ReadOnly);
- auto data = jsonFile.readAll();
- jsonFile.close();
- ProblemContainer problems;
- return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), path);
- }
- // get absolute path to expected storage, assuming default cache prefix
- QStringList getStorage(QString relative)
- {
- return {FS::PathCombine(cache->getBasePath("libraries"), relative)};
- }
-private
-slots:
- void initTestCase()
- {
- cache.reset(new HttpMetaCache());
- cache->addBase("libraries", QDir("libraries").absolutePath());
- dataDir = QDir(QFINDTESTDATA("testdata")).absolutePath();
- }
- void test_legacy()
- {
- Library test("test.package:testname:testversion");
- QCOMPARE(test.artifactPrefix(), QString("test.package:testname"));
- QCOMPARE(test.isNative(), false);
-
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(currentSystem, jar, native, native32, native64, QString());
- QCOMPARE(jar, getStorage("test/package/testname/testversion/testname-testversion.jar"));
- QCOMPARE(native, {});
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- }
- void test_legacy_url()
- {
- QStringList failedFiles;
- Library test("test.package:testname:testversion");
- test.setRepositoryURL("file://foo/bar");
- auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString());
- QCOMPARE(downloads.size(), 1);
- QCOMPARE(failedFiles, {});
- NetAction::Ptr dl = downloads[0];
- QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar"));
- }
- void test_legacy_url_local_broken()
- {
- Library test("test.package:testname:testversion");
- QCOMPARE(test.isNative(), false);
- QStringList failedFiles;
- test.setHint("local");
- auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString());
- QCOMPARE(downloads.size(), 0);
- QCOMPARE(failedFiles, {"testname-testversion.jar"});
- }
- void test_legacy_url_local_override()
- {
- Library test("com.paulscode:codecwav:20101023");
- QCOMPARE(test.isNative(), false);
- QStringList failedFiles;
- test.setHint("local");
- auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QFINDTESTDATA("testdata"));
- QCOMPARE(downloads.size(), 0);
- qDebug() << failedFiles;
- QCOMPARE(failedFiles.size(), 0);
-
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(currentSystem, jar, native, native32, native64, QFINDTESTDATA("testdata"));
- QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()});
- QCOMPARE(native, {});
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- }
- void test_legacy_native()
- {
- Library test("test.package:testname:testversion");
- test.m_nativeClassifiers[OpSys::Os_Linux]="linux";
- QCOMPARE(test.isNative(), true);
- test.setRepositoryURL("file://foo/bar");
- {
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString());
- QCOMPARE(jar, {});
- QCOMPARE(native, getStorage("test/package/testname/testversion/testname-testversion-linux.jar"));
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- QStringList failedFiles;
- auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 1);
- QCOMPARE(failedFiles, {});
- auto dl = dls[0];
- QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar"));
- }
- }
- void test_legacy_native_arch()
- {
- Library test("test.package:testname:testversion");
- test.m_nativeClassifiers[OpSys::Os_Linux]="linux-${arch}";
- test.m_nativeClassifiers[OpSys::Os_OSX]="osx-${arch}";
- test.m_nativeClassifiers[OpSys::Os_Windows]="windows-${arch}";
- QCOMPARE(test.isNative(), true);
- test.setRepositoryURL("file://foo/bar");
- {
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString());
- QCOMPARE(jar, {});
- QCOMPARE(native, {});
- QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-linux-32.jar"));
- QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar"));
- QStringList failedFiles;
- auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 2);
- QCOMPARE(failedFiles, {});
- QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar"));
- QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar"));
- }
- {
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(Os_Windows, jar, native, native32, native64, QString());
- QCOMPARE(jar, {});
- QCOMPARE(native, {});
- QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-windows-32.jar"));
- QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-windows-64.jar"));
- QStringList failedFiles;
- auto dls = test.getDownloads(Os_Windows, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 2);
- QCOMPARE(failedFiles, {});
- QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar"));
- QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar"));
- }
- {
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(Os_OSX, jar, native, native32, native64, QString());
- QCOMPARE(jar, {});
- QCOMPARE(native, {});
- QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-osx-32.jar"));
- QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-osx-64.jar"));
- QStringList failedFiles;
- auto dls = test.getDownloads(Os_OSX, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 2);
- QCOMPARE(failedFiles, {});
- QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar"));
- QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar"));
- }
- }
- void test_legacy_native_arch_local_override()
- {
- Library test("test.package:testname:testversion");
- test.m_nativeClassifiers[OpSys::Os_Linux]="linux-${arch}";
- test.setHint("local");
- QCOMPARE(test.isNative(), true);
- test.setRepositoryURL("file://foo/bar");
- {
- QStringList jar, native, native32, native64;
- test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QFINDTESTDATA("testdata"));
- QCOMPARE(jar, {});
- QCOMPARE(native, {});
- QCOMPARE(native32, {QFileInfo(QFINDTESTDATA("testdata/testname-testversion-linux-32.jar")).absoluteFilePath()});
- QCOMPARE(native64, {QFileInfo(QFINDTESTDATA("testdata") + "/testname-testversion-linux-64.jar").absoluteFilePath()});
- QStringList failedFiles;
- auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata"));
- QCOMPARE(dls.size(), 0);
- QCOMPARE(failedFiles, {QFileInfo(QFINDTESTDATA("testdata") + "/testname-testversion-linux-64.jar").absoluteFilePath()});
- }
- }
- void test_onenine()
- {
- auto test = readMojangJson(QFINDTESTDATA("testdata/lib-simple.json"));
- {
- QStringList jar, native, native32, native64;
- test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString());
- QCOMPARE(jar, getStorage("com/paulscode/codecwav/20101023/codecwav-20101023.jar"));
- QCOMPARE(native, {});
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- }
- {
- QStringList failedFiles;
- auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 1);
- QCOMPARE(failedFiles, {});
- QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"));
- }
- test->setHint("local");
- {
- QStringList jar, native, native32, native64;
- test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata"));
- QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()});
- QCOMPARE(native, {});
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- }
- {
- QStringList failedFiles;
- auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata"));
- QCOMPARE(dls.size(), 0);
- QCOMPARE(failedFiles, {});
- }
- }
- void test_onenine_local_override()
- {
- auto test = readMojangJson(QFINDTESTDATA("testdata/lib-simple.json"));
- test->setHint("local");
- {
- QStringList jar, native, native32, native64;
- test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata"));
- QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()});
- QCOMPARE(native, {});
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- }
- {
- QStringList failedFiles;
- auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata"));
- QCOMPARE(dls.size(), 0);
- QCOMPARE(failedFiles, {});
- }
- }
- void test_onenine_native()
- {
- auto test = readMojangJson(QFINDTESTDATA("testdata/lib-native.json"));
- QStringList jar, native, native32, native64;
- test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString());
- QCOMPARE(jar, QStringList());
- QCOMPARE(native, getStorage("org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"));
- QCOMPARE(native32, {});
- QCOMPARE(native64, {});
- QStringList failedFiles;
- auto dls = test->getDownloads(Os_OSX, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 1);
- QCOMPARE(failedFiles, {});
- QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"));
- }
- void test_onenine_native_arch()
- {
- auto test = readMojangJson(QFINDTESTDATA("testdata/lib-native-arch.json"));
- QStringList jar, native, native32, native64;
- test->getApplicableFiles(Os_Windows, jar, native, native32, native64, QString());
- QCOMPARE(jar, {});
- QCOMPARE(native, {});
- QCOMPARE(native32, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"));
- QCOMPARE(native64, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"));
- QStringList failedFiles;
- auto dls = test->getDownloads(Os_Windows, cache.get(), failedFiles, QString());
- QCOMPARE(dls.size(), 2);
- QCOMPARE(failedFiles, {});
- QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"));
- QCOMPARE(dls[1]->m_url, QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"));
- }
-private:
- std::unique_ptr<HttpMetaCache> cache;
- QString dataDir;
-};
-
-QTEST_GUILESS_MAIN(LibraryTest)
-
-#include "Library_test.moc"
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 5a6f8de0..9478b1b8 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -76,6 +76,7 @@
#include "mod/ModFolderModel.h"
#include "mod/ResourcePackFolderModel.h"
+#include "mod/ShaderPackFolderModel.h"
#include "mod/TexturePackFolderModel.h"
#include "WorldList.h"
@@ -115,6 +116,19 @@ private:
MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
: BaseInstance(globalSettings, settings, rootDir)
{
+ m_components.reset(new PackProfile(this));
+}
+
+void MinecraftInstance::saveNow()
+{
+ m_components->saveNow();
+}
+
+void MinecraftInstance::loadSpecificSettings()
+{
+ if (isSpecificSettingsLoaded())
+ return;
+
// Java Settings
auto javaOverride = m_settings->registerSetting("OverrideJava", false);
auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false);
@@ -124,64 +138,58 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO
auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride);
auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride);
- m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation);
- m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs);
- m_settings->registerOverride(globalSettings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
-
- // special!
- m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation);
- m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation);
- m_settings->registerPassthrough(globalSettings->getSetting("JavaArchitecture"), javaOrLocation);
-
- // Window Size
- auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
- m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting);
- m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting);
- m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting);
-
- // Memory
- auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
- m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting);
- m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting);
- m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting);
-
- // Minecraft launch method
- auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
- m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride);
-
- // Native library workarounds
- auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
- m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
- m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
-
- // Peformance related options
- auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
- m_settings->registerOverride(globalSettings->getSetting("EnableFeralGamemode"), performanceOverride);
- m_settings->registerOverride(globalSettings->getSetting("EnableMangoHud"), performanceOverride);
- m_settings->registerOverride(globalSettings->getSetting("UseDiscreteGpu"), performanceOverride);
-
- // Game time
- auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
- m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
- m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride);
+ if (auto global_settings = globalSettings()) {
+ m_settings->registerOverride(global_settings->getSetting("JavaPath"), javaOrLocation);
+ m_settings->registerOverride(global_settings->getSetting("JvmArgs"), javaOrArgs);
+ m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
+
+ // special!
+ m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
+
+ // Window Size
+ auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
+ m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting);
+ m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting);
+ m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting);
+
+ // Memory
+ auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
+ m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
+ m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
+ m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
+
+ // Minecraft launch method
+ auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
+ m_settings->registerOverride(global_settings->getSetting("MCLaunchMethod"), launchMethodOverride);
+
+ // Native library workarounds
+ auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
+ m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
+ m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
+
+ // Peformance related options
+ auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
+ m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
+ m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
+ m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
+
+ // Miscellaneous
+ auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
+ m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
+ m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
+
+ m_settings->set("InstanceType", "OneSix");
+ }
// Join server on launch, this does not have a global override
m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting("JoinServerOnLaunchAddress", "");
- // Miscellaneous
- auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
- m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
- m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
-
- m_settings->set("InstanceType", "OneSix");
+ qDebug() << "Instance-type specific settings were loaded!";
- m_components.reset(new PackProfile(this));
-}
-
-void MinecraftInstance::saveNow()
-{
- m_components->saveNow();
+ setSpecificSettingsLoaded(true);
}
QString MinecraftInstance::typeName() const
@@ -308,7 +316,7 @@ QDir MinecraftInstance::versionsPath() const
return QDir::current().absoluteFilePath("versions");
}
-QStringList MinecraftInstance::getClassPath() const
+QStringList MinecraftInstance::getClassPath()
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
@@ -323,7 +331,7 @@ QString MinecraftInstance::getMainClass() const
return profile->getMainClass();
}
-QStringList MinecraftInstance::getNativeJars() const
+QStringList MinecraftInstance::getNativeJars()
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
@@ -332,7 +340,7 @@ QStringList MinecraftInstance::getNativeJars() const
return nativeJars;
}
-QStringList MinecraftInstance::extraArguments() const
+QStringList MinecraftInstance::extraArguments()
{
auto list = BaseInstance::extraArguments();
auto version = getPackProfile();
@@ -358,7 +366,7 @@ QStringList MinecraftInstance::extraArguments() const
return list;
}
-QStringList MinecraftInstance::javaArguments() const
+QStringList MinecraftInstance::javaArguments()
{
QStringList args;
@@ -415,7 +423,7 @@ QStringList MinecraftInstance::javaArguments() const
return args;
}
-QMap<QString, QString> MinecraftInstance::getVariables() const
+QMap<QString, QString> MinecraftInstance::getVariables()
{
QMap<QString, QString> out;
out.insert("INST_NAME", name());
@@ -447,13 +455,11 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QProcessEnvironment env = createEnvironment();
#ifdef Q_OS_LINUX
- if (settings()->get("EnableMangoHud").toBool())
+ if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud)
{
auto preload = env.value("LD_PRELOAD", "") + ":libMangoHud_dlsym.so:libMangoHud.so";
- auto lib_path = env.value("LD_LIBRARY_PATH", "") + ":/usr/local/$LIB/mangohud/:/usr/$LIB/mangohud/";
env.insert("LD_PRELOAD", preload);
- env.insert("LD_LIBRARY_PATH", lib_path);
env.insert("MANGOHUD", "1");
}
@@ -707,7 +713,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
});
for(auto mod: modList)
{
- if(mod->type() == Mod::MOD_FOLDER)
+ if(mod->type() == ResourceType::FOLDER)
{
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue;
@@ -943,9 +949,9 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(new CreateGameFolders(pptr));
}
- if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool())
+ if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool())
{
- QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString();
+ QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString();
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress)));
}
@@ -1053,10 +1059,10 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
QString MinecraftInstance::launchMethod()
{
- return m_settings->get("MCLaunchMethod").toString();
+ return settings()->get("MCLaunchMethod").toString();
}
-JavaVersion MinecraftInstance::getJavaVersion() const
+JavaVersion MinecraftInstance::getJavaVersion()
{
return JavaVersion(settings()->get("JavaVersion").toString());
}
@@ -1085,18 +1091,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
return m_core_mod_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
+std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
{
if (!m_resource_pack_list)
{
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
- m_resource_pack_list->disableInteraction(isRunning());
- connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
+ m_resource_pack_list->enableInteraction(!isRunning());
+ connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
}
return m_resource_pack_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
+std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
{
if (!m_texture_pack_list)
{
@@ -1107,11 +1113,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
return m_texture_pack_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
+std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
{
if (!m_shader_pack_list)
{
- m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir()));
+ m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
m_shader_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
}
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
index 8e1c67f2..d62ac655 100644
--- a/launcher/minecraft/MinecraftInstance.h
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -7,6 +7,10 @@
#include "minecraft/launch/MinecraftServerTarget.h"
class ModFolderModel;
+class ResourceFolderModel;
+class ResourcePackFolderModel;
+class ShaderPackFolderModel;
+class TexturePackFolderModel;
class WorldList;
class GameOptions;
class LaunchStep;
@@ -20,6 +24,8 @@ public:
virtual ~MinecraftInstance() {};
virtual void saveNow() override;
+ void loadSpecificSettings() override;
+
// FIXME: remove
QString typeName() const override;
// FIXME: remove
@@ -70,24 +76,24 @@ public:
////// Mod Lists //////
std::shared_ptr<ModFolderModel> loaderModList() const;
std::shared_ptr<ModFolderModel> coreModList() const;
- std::shared_ptr<ModFolderModel> resourcePackList() const;
- std::shared_ptr<ModFolderModel> texturePackList() const;
- std::shared_ptr<ModFolderModel> shaderPackList() const;
+ std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
+ std::shared_ptr<TexturePackFolderModel> texturePackList() const;
+ std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
std::shared_ptr<WorldList> worldList() const;
std::shared_ptr<GameOptions> gameOptionsModel() const;
////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
- QStringList extraArguments() const override;
+ QStringList extraArguments() override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
QList<Mod*> getJarMods() const;
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
/// get arguments passed to java
- QStringList javaArguments() const;
+ QStringList javaArguments();
/// get variables for launch command variable substitution/environment
- QMap<QString, QString> getVariables() const override;
+ QMap<QString, QString> getVariables() override;
/// create an environment for launching processes
QProcessEnvironment createEnvironment() override;
@@ -103,16 +109,16 @@ public:
QString getStatusbarDescription() override;
// FIXME: remove
- virtual QStringList getClassPath() const;
+ virtual QStringList getClassPath();
// FIXME: remove
- virtual QStringList getNativeJars() const;
+ virtual QStringList getNativeJars();
// FIXME: remove
virtual QString getMainClass() const;
// FIXME: remove
virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
- virtual JavaVersion getJavaVersion() const;
+ virtual JavaVersion getJavaVersion();
protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
@@ -123,9 +129,9 @@ protected: // data
std::shared_ptr<PackProfile> m_components;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
- mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
- mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
- mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
+ mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
+ mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
+ mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
};
diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp
index 0ce0c347..3a3aa864 100644
--- a/launcher/minecraft/MinecraftUpdate.cpp
+++ b/launcher/minecraft/MinecraftUpdate.cpp
@@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask()
m_tasks.clear();
// create folders
{
- m_tasks.append(std::make_shared<FoldersTask>(m_inst));
+ m_tasks.append(new FoldersTask(m_inst));
}
// add metadata update task if necessary
@@ -53,23 +53,23 @@ void MinecraftUpdate::executeTask()
auto task = components->getCurrentTask();
if(task)
{
- m_tasks.append(task.unwrap());
+ m_tasks.append(task);
}
}
// libraries download
{
- m_tasks.append(std::make_shared<LibrariesTask>(m_inst));
+ m_tasks.append(new LibrariesTask(m_inst));
}
// FML libraries download and copy into the instance
{
- m_tasks.append(std::make_shared<FMLLibrariesTask>(m_inst));
+ m_tasks.append(new FMLLibrariesTask(m_inst));
}
// assets update
{
- m_tasks.append(std::make_shared<AssetUpdateTask>(m_inst));
+ m_tasks.append(new AssetUpdateTask(m_inst));
}
if(!m_preFailure.isEmpty())
diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h
index acf2eb86..c9cf8624 100644
--- a/launcher/minecraft/MinecraftUpdate.h
+++ b/launcher/minecraft/MinecraftUpdate.h
@@ -50,7 +50,7 @@ private:
private:
MinecraftInstance *m_inst = nullptr;
- QList<std::shared_ptr<Task>> m_tasks;
+ QList<Task::Ptr> m_tasks;
QString m_preFailure;
int m_currentTask = -1;
bool m_abort = false;
diff --git a/launcher/minecraft/MojangVersionFormat_test.cpp b/launcher/minecraft/MojangVersionFormat_test.cpp
deleted file mode 100644
index 71df784b..00000000
--- a/launcher/minecraft/MojangVersionFormat_test.cpp
+++ /dev/null
@@ -1,53 +0,0 @@
-#include <QTest>
-#include <QDebug>
-
-#include "minecraft/MojangVersionFormat.h"
-
-class MojangVersionFormatTest : public QObject
-{
- Q_OBJECT
-
- static QJsonDocument readJson(const QString path)
- {
- QFile jsonFile(path);
- jsonFile.open(QIODevice::ReadOnly);
- auto data = jsonFile.readAll();
- jsonFile.close();
- return QJsonDocument::fromJson(data);
- }
- static void writeJson(const char *file, QJsonDocument doc)
- {
- QFile jsonFile(file);
- jsonFile.open(QIODevice::WriteOnly | QIODevice::Text);
- auto data = doc.toJson(QJsonDocument::Indented);
- qDebug() << QString::fromUtf8(data);
- jsonFile.write(data);
- jsonFile.close();
- }
-
-private
-slots:
- void test_Through_Simple()
- {
- QJsonDocument doc = readJson(QFINDTESTDATA("testdata/1.9-simple.json"));
- auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json");
- auto doc2 = MojangVersionFormat::versionFileToJson(vfile);
- writeJson("1.9-simple-passthorugh.json", doc2);
-
- QCOMPARE(doc.toJson(), doc2.toJson());
- }
-
- void test_Through()
- {
- QJsonDocument doc = readJson(QFINDTESTDATA("testdata/1.9.json"));
- auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json");
- auto doc2 = MojangVersionFormat::versionFileToJson(vfile);
- writeJson("1.9-passthorugh.json", doc2);
- QCOMPARE(doc.toJson(), doc2.toJson());
- }
-};
-
-QTEST_GUILESS_MAIN(MojangVersionFormatTest)
-
-#include "MojangVersionFormat_test.moc"
-
diff --git a/launcher/minecraft/ParseUtils_test.cpp b/launcher/minecraft/ParseUtils_test.cpp
deleted file mode 100644
index 7721a46d..00000000
--- a/launcher/minecraft/ParseUtils_test.cpp
+++ /dev/null
@@ -1,43 +0,0 @@
-#include <QTest>
-
-#include "minecraft/ParseUtils.h"
-
-class ParseUtilsTest : public QObject
-{
- Q_OBJECT
-private
-slots:
- void test_Through_data()
- {
- QTest::addColumn<QString>("timestamp");
- const char * timestamps[] =
- {
- "2016-02-29T13:49:54+01:00",
- "2016-02-26T15:21:11+00:01",
- "2016-02-24T15:52:36+01:13",
- "2016-02-18T17:41:00+00:00",
- "2016-02-17T15:23:19+00:00",
- "2016-02-16T15:22:39+09:22",
- "2016-02-10T15:06:41+00:00",
- "2016-02-04T15:28:02-05:33"
- };
- for(unsigned i = 0; i < (sizeof(timestamps) / sizeof(const char *)); i++)
- {
- QTest::newRow(timestamps[i]) << QString(timestamps[i]);
- }
- }
- void test_Through()
- {
- QFETCH(QString, timestamp);
-
- auto time_parsed = timeFromS3Time(timestamp);
- auto time_serialized = timeToS3Time(time_parsed);
-
- QCOMPARE(time_serialized, timestamp);
- }
-
-};
-
-QTEST_GUILESS_MAIN(ParseUtilsTest)
-
-#include "ParseUtils_test.moc"
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/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index a5c6f542..73d570f1 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -238,7 +238,7 @@ void MinecraftAccount::authFailed(QString reason)
}
bool MinecraftAccount::isActive() const {
- return m_currentTask;
+ return !m_currentTask.isNull();
}
bool MinecraftAccount::shouldRefresh() const {
diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp
index 152485b3..ca55cd2e 100644
--- a/launcher/minecraft/launch/DirectJavaLaunch.cpp
+++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp
@@ -21,6 +21,8 @@
#include <FileSystem.h>
#include <Commandline.h>
+#include "Application.h"
+
#ifdef Q_OS_LINUX
#include "gamemode_client.h"
#endif
@@ -86,7 +88,7 @@ void DirectJavaLaunch::executeTask()
}
#ifdef Q_OS_LINUX
- if (instance->settings()->get("EnableFeralGamemode").toBool())
+ if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index 63e4d90f..ce477ad7 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -181,7 +181,7 @@ void LauncherPartLaunch::executeTask()
}
#ifdef Q_OS_LINUX
- if (instance->settings()->get("EnableFeralGamemode").toBool())
+ if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index 588d76e3..39023f69 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -36,130 +36,77 @@
#include "Mod.h"
+#include <QDebug>
#include <QDir>
#include <QString>
+#include <QRegularExpression>
-#include <FileSystem.h>
-#include <QDebug>
-
-#include "Application.h"
#include "MetadataHandler.h"
+#include "Version.h"
-namespace {
-
-ModDetails invalidDetails;
-
-}
-
-Mod::Mod(const QFileInfo& file)
+Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
- repath(file);
- m_changedDateTime = file.lastModified();
+ m_enabled = (file.suffix() != "disabled");
}
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
- : m_file(mods_dir.absoluteFilePath(metadata.filename))
- , m_internal_id(metadata.filename)
- , m_name(metadata.name)
-{
- if (m_file.isDir()) {
- m_type = MOD_FOLDER;
- } else {
- if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar"))
- m_type = MOD_ZIPFILE;
- else if (metadata.filename.endsWith(".litemod"))
- m_type = MOD_LITEMOD;
- else
- m_type = MOD_SINGLEFILE;
- }
-
- m_enabled = true;
- m_changedDateTime = m_file.lastModified();
-
- m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
+ : Mod(mods_dir.absoluteFilePath(metadata.filename))
+{
+ m_name = metadata.name;
+ m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
-void Mod::repath(const QFileInfo& file)
+void Mod::setStatus(ModStatus status)
{
- m_file = file;
- QString name_base = file.fileName();
-
- m_type = Mod::MOD_UNKNOWN;
-
- m_internal_id = name_base;
-
- if (m_file.isDir()) {
- m_type = MOD_FOLDER;
- m_name = name_base;
- } else if (m_file.isFile()) {
- if (name_base.endsWith(".disabled")) {
- m_enabled = false;
- name_base.chop(9);
- } else {
- m_enabled = true;
- }
- if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) {
- m_type = MOD_ZIPFILE;
- name_base.chop(4);
- } else if (name_base.endsWith(".litemod")) {
- m_type = MOD_LITEMOD;
- name_base.chop(8);
- } else {
- m_type = MOD_SINGLEFILE;
- }
- m_name = name_base;
- }
+ m_local_details.status = status;
}
-
-auto Mod::enable(bool value) -> bool
+void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
{
- if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
- return false;
-
- if (m_enabled == value)
- return false;
-
- QString path = m_file.absoluteFilePath();
- QFile file(path);
- if (value) {
- if (!path.endsWith(".disabled"))
- return false;
- path.chop(9);
-
- if (!file.rename(path))
- return false;
- } else {
- path += ".disabled";
-
- if (!file.rename(path))
- return false;
- }
-
if (status() == ModStatus::NoMetadata)
- repath(QFileInfo(path));
+ setStatus(ModStatus::Installed);
- m_enabled = value;
- return true;
+ m_local_details.metadata = metadata;
}
-void Mod::setStatus(ModStatus status)
+std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
{
- if (m_localDetails) {
- m_localDetails->status = status;
- } else {
- m_temp_status = status;
+ auto cast_other = dynamic_cast<Mod const*>(&other);
+ if (!cast_other)
+ return Resource::compare(other, type);
+
+ switch (type) {
+ default:
+ case SortType::ENABLED:
+ case SortType::NAME:
+ case SortType::DATE: {
+ auto res = Resource::compare(other, type);
+ if (res.first != 0)
+ return res;
+ }
+ case SortType::VERSION: {
+ auto this_ver = Version(version());
+ auto other_ver = Version(cast_other->version());
+ if (this_ver > other_ver)
+ return { 1, type == SortType::VERSION };
+ if (this_ver < other_ver)
+ return { -1, type == SortType::VERSION };
+ }
}
+ return { 0, false };
}
-void Mod::setMetadata(const Metadata::ModStruct& metadata)
+
+bool Mod::applyFilter(QRegularExpression filter) const
{
- if (status() == ModStatus::NoMetadata)
- setStatus(ModStatus::Installed);
+ if (filter.match(description()).hasMatch())
+ return true;
- if (m_localDetails) {
- m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
- } else {
- m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
+ for (auto& author : authors()) {
+ if (filter.match(author).hasMatch()) {
+ return true;
+ }
}
+
+ return Resource::applyFilter(filter);
}
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
@@ -175,13 +122,12 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
}
}
- m_type = MOD_UNKNOWN;
- return FS::deletePath(m_file.filePath());
+ return Resource::destroy();
}
auto Mod::details() const -> const ModDetails&
{
- return m_localDetails ? *m_localDetails : invalidDetails;
+ return m_local_details;
}
auto Mod::name() const -> QString
@@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
auto Mod::status() const -> ModStatus
{
- if (!m_localDetails)
- return m_temp_status;
return details().status;
}
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
{
- if (m_localDetails)
- return m_localDetails->metadata;
- return m_temp_metadata;
+ return m_local_details.metadata;
}
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
{
- if (m_localDetails)
- return m_localDetails->metadata;
- return m_temp_metadata;
+ return m_local_details.metadata;
}
-void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
+void Mod::finishResolvingWithDetails(ModDetails&& details)
{
- m_resolving = false;
- m_resolved = true;
- m_localDetails = details;
+ m_is_resolving = false;
+ m_is_resolved = true;
- setStatus(m_temp_status);
+ std::shared_ptr<Metadata::ModStruct> metadata = details.metadata;
+ if (details.status == ModStatus::Unknown)
+ details.status = m_local_details.status;
- if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
- setMetadata(*m_temp_metadata);
- m_temp_metadata.reset();
- }
+ m_local_details = std::move(details);
+ if (metadata)
+ setMetadata(std::move(metadata));
}
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index 7a13e44b..f336bec4 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -39,38 +39,23 @@
#include <QFileInfo>
#include <QList>
-#include "QObjectPtr.h"
+#include "Resource.h"
#include "ModDetails.h"
-class Mod : public QObject
+class Mod : public Resource
{
Q_OBJECT
public:
- enum ModType
- {
- MOD_UNKNOWN, //!< Indicates an unspecified mod type.
- MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
- MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
- MOD_FOLDER, //!< The mod is in a folder on the filesystem.
- MOD_LITEMOD, //!< The mod is a litemod
- };
-
using Ptr = shared_qobject_ptr<Mod>;
+ using WeakPtr = QPointer<Mod>;
Mod() = default;
Mod(const QFileInfo &file);
- explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
-
- auto fileinfo() const -> QFileInfo { return m_file; }
- auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; }
- auto internal_id() const -> QString { return m_internal_id; }
- auto type() const -> ModType { return m_type; }
- auto enabled() const -> bool { return m_enabled; }
-
- auto valid() const -> bool { return m_type != MOD_UNKNOWN; }
+ Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
+ Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
auto details() const -> const ModDetails&;
- auto name() const -> QString;
+ auto name() const -> QString override;
auto version() const -> QString;
auto homeurl() const -> QString;
auto description() const -> QString;
@@ -81,46 +66,17 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status);
- void setMetadata(const Metadata::ModStruct& metadata);
+ void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
+ void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
- auto enable(bool value) -> bool;
+ [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
+ [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
- // delete all the files of this mod
+ // Delete all the files of this mod
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);
-
- auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; }
- auto isResolving() const -> bool { return m_resolving; }
- auto resolutionTicket() const -> int { return m_resolutionTicket; }
-
- void setResolving(bool resolving, int resolutionTicket) {
- m_resolving = resolving;
- m_resolutionTicket = resolutionTicket;
- }
- void finishResolvingWithDetails(std::shared_ptr<ModDetails> details);
+ void finishResolvingWithDetails(ModDetails&& details);
protected:
- QFileInfo m_file;
- QDateTime m_changedDateTime;
-
- QString m_internal_id;
- /* Name as reported via the file name */
- QString m_name;
- ModType m_type = MOD_UNKNOWN;
-
- /* If the mod has metadata, this will be filled in the constructor, and passed to
- * the ModDetails when calling finishResolvingWithDetails */
- 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::NoMetadata;
-
- std::shared_ptr<ModDetails> m_localDetails;
-
- bool m_enabled = true;
- bool m_resolving = false;
- bool m_resolved = false;
- int m_resolutionTicket = 0;
+ ModDetails m_local_details;
};
diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h
index 3e0a7ab0..dd84b0a3 100644
--- a/launcher/minecraft/mod/ModDetails.h
+++ b/launcher/minecraft/mod/ModDetails.h
@@ -46,34 +46,77 @@ enum class ModStatus {
Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present
+ Unknown, // Default status
};
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
- QString mod_id;
+ QString mod_id = {};
/* Human-readable name */
- QString name;
+ QString name = {};
/* Human-readable mod version */
- QString version;
+ QString version = {};
/* Human-readable minecraft version */
- QString mcversion;
+ QString mcversion = {};
/* URL for mod's home page */
- QString homeurl;
+ QString homeurl = {};
/* Human-readable description */
- QString description;
+ QString description = {};
/* List of the author's names */
- QStringList authors;
+ QStringList authors = {};
/* Installation status of the mod */
- ModStatus status;
+ ModStatus status = ModStatus::Unknown;
/* Metadata information, if any */
- std::shared_ptr<Metadata::ModStruct> metadata;
+ std::shared_ptr<Metadata::ModStruct> metadata = nullptr;
+
+ ModDetails() = default;
+
+ /** Metadata should be handled manually to properly set the mod status. */
+ ModDetails(ModDetails& other)
+ : mod_id(other.mod_id)
+ , name(other.name)
+ , version(other.version)
+ , mcversion(other.mcversion)
+ , homeurl(other.homeurl)
+ , description(other.description)
+ , authors(other.authors)
+ , status(other.status)
+ {}
+
+ ModDetails& operator=(ModDetails& other)
+ {
+ this->mod_id = other.mod_id;
+ this->name = other.name;
+ this->version = other.version;
+ this->mcversion = other.mcversion;
+ this->homeurl = other.homeurl;
+ this->description = other.description;
+ this->authors = other.authors;
+ this->status = other.status;
+
+ return *this;
+ }
+
+ ModDetails& operator=(ModDetails&& other)
+ {
+ this->mod_id = other.mod_id;
+ this->name = other.name;
+ this->version = other.version;
+ this->mcversion = other.mcversion;
+ this->homeurl = other.homeurl;
+ this->description = other.description;
+ this->authors = other.authors;
+ this->status = other.status;
+
+ return *this;
+ }
};
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
index 112d219e..13fed1c9 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -49,428 +49,53 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
-ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed)
+ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
- m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
- m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
- m_watcher = new QFileSystemWatcher(this);
- connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
-}
-
-void ModFolderModel::startWatching()
-{
- if(is_watching)
- return;
-
- update();
-
- // Watch the mods folder
- is_watching = m_watcher->addPath(m_dir.absolutePath());
- if (is_watching) {
- qDebug() << "Started watching " << m_dir.absolutePath();
- } 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()
-{
- if(!is_watching)
- return;
-
- is_watching = !m_watcher->removePath(m_dir.absolutePath());
- if (!is_watching) {
- qDebug() << "Stopped watching " << m_dir.absolutePath();
- } 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()
-{
- if (!isValid()) {
- return false;
- }
- if(m_update) {
- scheduled_update = true;
- return true;
- }
-
- auto index_dir = indexDir();
- auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed);
-
- m_update = task->result();
-
- QThreadPool *threadPool = QThreadPool::globalInstance();
- connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
-
- threadPool->start(task);
- return true;
-}
-
-void ModFolderModel::finishUpdate()
-{
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- auto currentList = modsIndex.keys();
- QSet<QString> currentSet(currentList.begin(), currentList.end());
- auto & newMods = m_update->mods;
- auto newList = newMods.keys();
- QSet<QString> newSet(newList.begin(), newList.end());
-#else
- QSet<QString> currentSet = modsIndex.keys().toSet();
- auto& newMods = m_update->mods;
- QSet<QString> newSet = newMods.keys().toSet();
-#endif
-
- // see if the kept mods changed in some way
- {
- QSet<QString> kept = currentSet;
- kept.intersect(newSet);
- for(auto& keptMod : kept) {
- auto newMod = newMods[keptMod];
- auto row = modsIndex[keptMod];
- 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());
- }
-
- mods[row] = newMod;
- resolveMod(mods[row]);
- emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
- }
- }
-
- // remove mods no longer present
- {
- QSet<QString> removed = currentSet;
- QList<int> removedRows;
- removed.subtract(newSet);
- for(auto & removedMod: removed) {
- removedRows.append(modsIndex[removedMod]);
- }
- std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
- for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
- int removedIndex = *iter;
- beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
- auto removedIter = mods.begin() + removedIndex;
- if((*removedIter)->isResolving()) {
- activeTickets.remove((*removedIter)->resolutionTicket());
- }
-
- mods.erase(removedIter);
- endRemoveRows();
- }
- }
-
- // add new mods to the end
- {
- QSet<QString> added = newSet;
- added.subtract(currentSet);
-
- // When you have a Qt build with assertions turned on, proceeding here will abort the application
- if (added.size() > 0) {
- beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
- for (auto& addedMod : added) {
- mods.append(newMods[addedMod]);
- resolveMod(mods.last());
- }
- endInsertRows();
- }
- }
-
- // update index
- {
- modsIndex.clear();
- int idx = 0;
- for(auto mod: mods) {
- modsIndex[mod->internal_id()] = idx;
- idx++;
- }
- }
-
- m_update.reset();
-
- emit updateFinished();
-
- if(scheduled_update) {
- scheduled_update = false;
- update();
- }
-}
-
-void ModFolderModel::resolveMod(Mod::Ptr m)
-{
- if(!m->shouldResolve()) {
- return;
- }
-
- auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
- auto result = task->result();
- result->id = m->internal_id();
- activeTickets.insert(nextResolutionTicket, result);
- m->setResolving(true, nextResolutionTicket);
- nextResolutionTicket++;
- QThreadPool *threadPool = QThreadPool::globalInstance();
- connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
- threadPool->start(task);
-}
-
-void ModFolderModel::finishModParse(int token)
-{
- auto iter = activeTickets.find(token);
- if(iter == activeTickets.end()) {
- return;
- }
- auto result = *iter;
- activeTickets.remove(token);
- int row = modsIndex[result->id];
- auto mod = mods[row];
- mod->finishResolvingWithDetails(result->details);
- emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
-}
-
-void ModFolderModel::disableInteraction(bool disabled)
-{
- if (interaction_disabled == disabled) {
- return;
- }
- interaction_disabled = disabled;
- if(size()) {
- emit dataChanged(index(0), index(size() - 1));
- }
-}
-
-void ModFolderModel::directoryChanged(QString path)
-{
- update();
-}
-
-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)
-{
- if(interaction_disabled) {
- return false;
- }
-
- // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
- auto originalPath = FS::NormalizePath(filename);
- QFileInfo fileinfo(originalPath);
-
- if (!fileinfo.exists() || !fileinfo.isReadable())
- {
- qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
- return false;
- }
- qDebug() << "installing: " << fileinfo.absoluteFilePath();
-
- Mod installedMod(fileinfo);
- if (!installedMod.valid())
- {
- qDebug() << originalPath << "is not a valid mod. Ignoring it.";
- return false;
- }
-
- auto type = installedMod.type();
- if (type == Mod::MOD_UNKNOWN)
- {
- qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
- return false;
- }
-
- auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
- if(originalPath == newpath)
- {
- qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
- return false;
- }
-
- if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
- {
- if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
- {
- if(!QFile::remove(newpath))
- {
- // FIXME: report error in a user-visible way
- qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
- return false;
- }
- qDebug() << newpath << "has been deleted.";
- }
- if (!QFile::copy(fileinfo.filePath(), newpath))
- {
- qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
- // FIXME: report error in a user-visible way
- return false;
- }
- FS::updateTimestamp(newpath);
- QFileInfo newpathInfo(newpath);
- installedMod.repath(newpathInfo);
- update();
- return true;
- }
- else if (type == Mod::MOD_FOLDER)
- {
- QString from = fileinfo.filePath();
- if(QFile::exists(newpath))
- {
- qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
- return false;
- }
-
- if (!FS::copy(from, newpath)())
- {
- qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
- return false;
- }
- QFileInfo newpathInfo(newpath);
- installedMod.repath(newpathInfo);
- update();
- return true;
- }
- 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) {
- return false;
- }
-
- if(indexes.isEmpty())
- return true;
-
- for (auto index: indexes)
- {
- if(index.column() != 0) {
- continue;
- }
- setModStatus(index.row(), enable);
- }
- return true;
-}
-
-bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
-{
- if(interaction_disabled) {
- return false;
- }
-
- if(indexes.isEmpty())
- return true;
-
- for (auto i: indexes)
- {
- if(i.column() != 0) {
- continue;
- }
- auto m = mods[i.row()];
- auto index_dir = indexDir();
- m->destroy(index_dir);
- }
- return true;
-}
-
-int ModFolderModel::columnCount(const QModelIndex &parent) const
-{
- return NUM_COLUMNS;
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{
- if (!index.isValid())
- return QVariant();
+ if (!validateIndex(index))
+ return {};
int row = index.row();
int column = index.column();
- if (row < 0 || row >= mods.size())
- return QVariant();
-
switch (role)
{
case Qt::DisplayRole:
switch (column)
{
case NameColumn:
- return mods[row]->name();
+ return m_resources[row]->name();
case VersionColumn: {
- switch(mods[row]->type()) {
- case Mod::MOD_FOLDER:
+ switch(m_resources[row]->type()) {
+ case ResourceType::FOLDER:
return tr("Folder");
- case Mod::MOD_SINGLEFILE:
+ case ResourceType::SINGLEFILE:
return tr("File");
default:
break;
}
- return mods[row]->version();
+ return at(row)->version();
}
case DateColumn:
- return mods[row]->dateTimeChanged();
+ return m_resources[row]->dateTimeChanged();
default:
return QVariant();
}
case Qt::ToolTipRole:
- return mods[row]->internal_id();
+ return m_resources[row]->internal_id();
case Qt::CheckStateRole:
switch (column)
{
case ActiveColumn:
- return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
+ return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
@@ -479,61 +104,6 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
}
}
-bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
-{
- if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
- {
- return false;
- }
-
- if (role == Qt::CheckStateRole)
- {
- return setModStatus(index.row(), Toggle);
- }
- return false;
-}
-
-bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
-{
- if(row < 0 || row >= mods.size()) {
- return false;
- }
-
- auto &mod = mods[row];
- bool desiredStatus;
- switch(action) {
- case Enable:
- desiredStatus = true;
- break;
- case Disable:
- desiredStatus = false;
- break;
- case Toggle:
- default:
- desiredStatus = !mod->enabled();
- break;
- }
-
- if(desiredStatus == mod->enabled()) {
- return true;
- }
-
- // preserve the row, but change its ID
- auto oldId = mod->internal_id();
- if(!mod->enable(!mod->enabled())) {
- return false;
- }
- 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?
- }
- modsIndex.remove(oldId);
- modsIndex[newId] = row;
- emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
- return true;
-}
-
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
@@ -573,65 +143,142 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
-Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
+int ModFolderModel::columnCount(const QModelIndex &parent) const
{
- Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
- auto flags = defaultFlags;
- if(interaction_disabled) {
- flags &= ~Qt::ItemIsDropEnabled;
- }
- else
- {
- flags |= Qt::ItemIsDropEnabled;
- if(index.isValid()) {
- flags |= Qt::ItemIsUserCheckable;
- }
- }
- return flags;
+ return NUM_COLUMNS;
}
-Qt::DropActions ModFolderModel::supportedDropActions() const
+Task* ModFolderModel::createUpdateTask()
{
- // copy from outside, move from within and other mod lists
- return Qt::CopyAction | Qt::MoveAction;
+ auto index_dir = indexDir();
+ auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
+ m_first_folder_load = false;
+ return task;
}
-QStringList ModFolderModel::mimeTypes() const
+Task* ModFolderModel::createParseTask(Resource& resource)
{
- QStringList types;
- types << "text/uri-list";
- return types;
+ return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
}
-bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
+bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
- if (action == Qt::IgnoreAction)
- {
- return true;
+ for(auto mod : allMods()){
+ if(mod->fileinfo().fileName() == filename){
+ auto index_dir = indexDir();
+ mod->destroy(index_dir, preserve_metadata);
+
+ update();
+
+ return true;
+ }
}
- // check if the action is supported
- if (!data || !(action & supportedDropActions()))
- {
+ return false;
+}
+
+bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
+{
+ if(!m_can_interact) {
return false;
}
- // files dropped from outside?
- if (data->hasUrls())
+ if(indexes.isEmpty())
+ return true;
+
+ for (auto i: indexes)
{
- auto urls = data->urls();
- for (auto url : urls)
- {
- // only local files may be dropped...
- if (!url.isLocalFile())
- {
- continue;
- }
- // TODO: implement not only copy, but also move
- // FIXME: handle errors here
- installMod(url.toLocalFile());
+ if(i.column() != 0) {
+ continue;
}
- return true;
+ auto m = at(i.row());
+ auto index_dir = indexDir();
+ m->destroy(index_dir);
}
- return false;
+
+ update();
+
+ return true;
+}
+
+bool ModFolderModel::isValid()
+{
+ return m_dir.exists() && m_dir.isReadable();
+}
+
+bool ModFolderModel::startWatching()
+{
+ // Remove orphaned metadata next time
+ m_first_folder_load = true;
+ return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
+}
+
+bool ModFolderModel::stopWatching()
+{
+ return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
+}
+
+auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*>
+{
+ QList<Mod*> selected_resources;
+ for (auto i : indexes) {
+ if(i.column() != 0)
+ continue;
+
+ selected_resources.push_back(at(i.row()));
+ }
+ return selected_resources;
+}
+
+auto ModFolderModel::allMods() -> QList<Mod*>
+{
+ QList<Mod*> mods;
+
+ for (auto& res : m_resources) {
+ mods.append(static_cast<Mod*>(res.get()));
+ }
+
+ return mods;
+}
+
+void ModFolderModel::onUpdateSucceeded()
+{
+ auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result();
+
+ auto& new_mods = update_results->mods;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ auto current_list = m_resources_index.keys();
+ QSet<QString> current_set(current_list.begin(), current_list.end());
+
+ auto new_list = new_mods.keys();
+ QSet<QString> new_set(new_list.begin(), new_list.end());
+#else
+ QSet<QString> current_set(m_resources_index.keys().toSet());
+ QSet<QString> new_set(new_mods.keys().toSet());
+#endif
+
+ applyUpdates(current_set, new_set, new_mods);
+}
+
+void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
+{
+ auto iter = m_active_parse_tasks.constFind(ticket);
+ if (iter == m_active_parse_tasks.constEnd())
+ return;
+
+ int row = m_resources_index[mod_id];
+
+ auto parse_task = *iter;
+ auto cast_task = static_cast<LocalModParseTask*>(parse_task.get());
+
+ Q_ASSERT(cast_task->token() == ticket);
+
+ auto resource = find(mod_id);
+
+ auto result = cast_task->result();
+ if (result && resource)
+ resource->finishResolvingWithDetails(std::move(result->details));
+
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index a7d3ece0..93980319 100644
--- a/launcher/minecraft/mod/ModFolderModel.h
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -44,6 +44,7 @@
#include <QAbstractListModel>
#include "Mod.h"
+#include "ResourceFolderModel.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
@@ -56,7 +57,7 @@ class QFileSystemWatcher;
* A legacy mod list.
* Backed by a folder.
*/
-class ModFolderModel : public QAbstractListModel
+class ModFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
@@ -75,105 +76,38 @@ public:
};
ModFolderModel(const QString &dir, bool is_indexed = false);
- virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
- virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
- Qt::DropActions supportedDropActions() const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
- /// flags, mostly to support drag&drop
- virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
- QStringList mimeTypes() const override;
- bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ int columnCount(const QModelIndex &parent) const override;
- virtual int rowCount(const QModelIndex &) const override
- {
- return size();
- }
-
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
- virtual int columnCount(const QModelIndex &parent) const override;
-
- size_t size() const
- {
- return mods.size();
- }
- ;
- bool empty() const
- {
- return size() == 0;
- }
- Mod& operator[](size_t index)
- {
- return *mods[index];
- }
- const Mod& at(size_t index) const
- {
- return *mods.at(index);
- }
-
- /// Reloads the mod list and returns true if the list changed.
- bool update();
-
- /**
- * Adds the given mod to the list at the given index - if the list supports custom ordering
- */
- bool installMod(const QString& filename);
+ [[nodiscard]] Task* createUpdateTask() override;
+ [[nodiscard]] Task* createParseTask(Resource&) override;
+ bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
/// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes);
- /// Enable or disable listed mods
- bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
-
- void startWatching();
- void stopWatching();
-
bool isValid();
- QDir& dir()
- {
- return m_dir;
- }
-
- QDir indexDir()
- {
- return { QString("%1/.index").arg(dir().absolutePath()) };
- }
+ bool startWatching() override;
+ bool stopWatching() override;
- const QList<Mod::Ptr>& allMods()
- {
- return mods;
- }
+ QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
- auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
+ auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
+ auto allMods() -> QList<Mod*>;
-public slots:
- void disableInteraction(bool disabled);
+ RESOURCE_HELPERS(Mod)
private
slots:
- void directoryChanged(QString path);
- void finishUpdate();
- void finishModParse(int token);
-
-signals:
- void updateFinished();
-
-private:
- void resolveMod(Mod::Ptr m);
- bool setModStatus(int index, ModStatusAction action);
+ void onUpdateSucceeded() override;
+ void onParseSucceeded(int ticket, QString resource_id) override;
protected:
- QFileSystemWatcher *m_watcher;
- bool is_watching = false;
- ModFolderLoadTask::ResultPtr m_update;
- bool scheduled_update = false;
- bool interaction_disabled = false;
- QDir m_dir;
bool m_is_indexed;
- QMap<QString, int> modsIndex;
- QMap<int, LocalModParseTask::ResultPtr> activeTickets;
- int nextResolutionTicket = 0;
- QList<Mod::Ptr> mods;
+ bool m_first_folder_load = true;
};
diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp
deleted file mode 100644
index b4d37ce5..00000000
--- a/launcher/minecraft/mod/ModFolderModel_test.cpp
+++ /dev/null
@@ -1,92 +0,0 @@
-// 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/>.
-*
-* This file incorporates work covered by the following copyright and
-* permission notice:
-*
-* Copyright 2013-2021 MultiMC Contributors
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
-
-#include <QTest>
-#include <QTemporaryDir>
-
-#include "FileSystem.h"
-#include "minecraft/mod/ModFolderModel.h"
-
-class ModFolderModelTest : public QObject
-{
- Q_OBJECT
-
-private
-slots:
- // test for GH-1178 - install a folder with files to a mod list
- void test_1178()
- {
- // source
- QString source = QFINDTESTDATA("testdata/test_folder");
-
- // sanity check
- QVERIFY(!source.endsWith('/'));
-
- auto verify = [](QString path)
- {
- QDir target_dir(FS::PathCombine(path, "test_folder"));
- QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
- QVERIFY(target_dir.entryList().contains("assets"));
- };
-
- // 1. test with no trailing /
- {
- QString folder = source;
- QTemporaryDir tempDir;
- QEventLoop loop;
- ModFolderModel m(tempDir.path(), true);
- connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
- m.installMod(folder);
- loop.exec();
- verify(tempDir.path());
- }
-
- // 2. test with trailing /
- {
- QString folder = source + '/';
- QTemporaryDir tempDir;
- QEventLoop loop;
- ModFolderModel m(tempDir.path(), true);
- connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
- m.installMod(folder);
- loop.exec();
- verify(tempDir.path());
- }
- }
-};
-
-QTEST_GUILESS_MAIN(ModFolderModelTest)
-
-#include "ModFolderModel_test.moc"
diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp
new file mode 100644
index 00000000..0fbcfd7c
--- /dev/null
+++ b/launcher/minecraft/mod/Resource.cpp
@@ -0,0 +1,147 @@
+#include "Resource.h"
+
+#include <QRegularExpression>
+
+#include "FileSystem.h"
+
+Resource::Resource(QObject* parent) : QObject(parent) {}
+
+Resource::Resource(QFileInfo file_info) : QObject()
+{
+ setFile(file_info);
+}
+
+void Resource::setFile(QFileInfo file_info)
+{
+ m_file_info = file_info;
+ parseFile();
+}
+
+void Resource::parseFile()
+{
+ QString file_name{ m_file_info.fileName() };
+
+ m_type = ResourceType::UNKNOWN;
+
+ m_internal_id = file_name;
+
+ if (m_file_info.isDir()) {
+ m_type = ResourceType::FOLDER;
+ m_name = file_name;
+ } else if (m_file_info.isFile()) {
+ if (file_name.endsWith(".disabled")) {
+ file_name.chop(9);
+ m_enabled = false;
+ }
+
+ if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) {
+ m_type = ResourceType::ZIPFILE;
+ file_name.chop(4);
+ } else if (file_name.endsWith(".litemod")) {
+ m_type = ResourceType::LITEMOD;
+ file_name.chop(8);
+ } else {
+ m_type = ResourceType::SINGLEFILE;
+ }
+
+ m_name = file_name;
+ }
+
+ m_changed_date_time = m_file_info.lastModified();
+}
+
+static void removeThePrefix(QString& string)
+{
+ QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
+ string.remove(regex);
+ string = string.trimmed();
+}
+
+std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const
+{
+ switch (type) {
+ default:
+ case SortType::ENABLED:
+ if (enabled() && !other.enabled())
+ return { 1, type == SortType::ENABLED };
+ if (!enabled() && other.enabled())
+ return { -1, type == SortType::ENABLED };
+ case SortType::NAME: {
+ QString this_name{ name() };
+ QString other_name{ other.name() };
+
+ removeThePrefix(this_name);
+ removeThePrefix(other_name);
+
+ auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
+ if (compare_result != 0)
+ return { compare_result, type == SortType::NAME };
+ }
+ case SortType::DATE:
+ if (dateTimeChanged() > other.dateTimeChanged())
+ return { 1, type == SortType::DATE };
+ if (dateTimeChanged() < other.dateTimeChanged())
+ return { -1, type == SortType::DATE };
+ }
+
+ return { 0, false };
+}
+
+bool Resource::applyFilter(QRegularExpression filter) const
+{
+ return filter.match(name()).hasMatch();
+}
+
+bool Resource::enable(EnableAction action)
+{
+ if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER)
+ return false;
+
+
+ QString path = m_file_info.absoluteFilePath();
+ QFile file(path);
+
+ bool enable = true;
+ switch (action) {
+ case EnableAction::ENABLE:
+ enable = true;
+ break;
+ case EnableAction::DISABLE:
+ enable = false;
+ break;
+ case EnableAction::TOGGLE:
+ default:
+ enable = !enabled();
+ break;
+ }
+
+ if (m_enabled == enable)
+ return false;
+
+ if (enable) {
+ // m_enabled is false, but there's no '.disabled' suffix.
+ // TODO: Report error?
+ if (!path.endsWith(".disabled"))
+ return false;
+ path.chop(9);
+
+ if (!file.rename(path))
+ return false;
+ } else {
+ path += ".disabled";
+
+ if (!file.rename(path))
+ return false;
+ }
+
+ setFile(QFileInfo(path));
+
+ m_enabled = enable;
+ return true;
+}
+
+bool Resource::destroy()
+{
+ m_type = ResourceType::UNKNOWN;
+ return FS::deletePath(m_file_info.filePath());
+}
diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h
new file mode 100644
index 00000000..f9bd811e
--- /dev/null
+++ b/launcher/minecraft/mod/Resource.h
@@ -0,0 +1,117 @@
+#pragma once
+
+#include <QDateTime>
+#include <QFileInfo>
+#include <QObject>
+#include <QPointer>
+
+#include "QObjectPtr.h"
+
+enum class ResourceType {
+ UNKNOWN, //!< Indicates an unspecified resource type.
+ ZIPFILE, //!< The resource is a zip file containing the resource's class files.
+ SINGLEFILE, //!< The resource is a single file (not a zip file).
+ FOLDER, //!< The resource is in a folder on the filesystem.
+ LITEMOD, //!< The resource is a litemod
+};
+
+enum class SortType {
+ NAME,
+ DATE,
+ VERSION,
+ ENABLED,
+ PACK_FORMAT
+};
+
+enum class EnableAction {
+ ENABLE,
+ DISABLE,
+ TOGGLE
+};
+
+/** General class for managed resources. It mirrors a file in disk, with some more info
+ * for display and house-keeping purposes.
+ *
+ * Subclass it to add additional data / behavior, such as Mods or Resource packs.
+ */
+class Resource : public QObject {
+ Q_OBJECT
+ Q_DISABLE_COPY(Resource)
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+ using WeakPtr = QPointer<Resource>;
+
+ Resource(QObject* parent = nullptr);
+ Resource(QFileInfo file_info);
+ Resource(QString file_path) : Resource(QFileInfo(file_path)) {}
+
+ ~Resource() override = default;
+
+ void setFile(QFileInfo file_info);
+ void parseFile();
+
+ [[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; }
+ [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; }
+ [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
+ [[nodiscard]] auto type() const -> ResourceType { return m_type; }
+ [[nodiscard]] bool enabled() const { return m_enabled; }
+
+ [[nodiscard]] virtual auto name() const -> QString { return m_name; }
+ [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
+
+ /** Compares two Resources, for sorting purposes, considering a ascending order, returning:
+ * > 0: 'this' comes after 'other'
+ * = 0: 'this' is equal to 'other'
+ * < 0: 'this' comes before 'other'
+ *
+ * The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
+ */
+ [[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>;
+
+ /** Returns whether the given filter should filter out 'this' (false),
+ * or if such filter includes the Resource (true).
+ */
+ [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const;
+
+ /** Changes the enabled property, according to 'action'.
+ *
+ * Returns whether a change was applied to the Resource's properties.
+ */
+ bool enable(EnableAction action);
+
+ [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; }
+ [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; }
+ [[nodiscard]] auto isResolved() const -> bool { return m_is_resolved; }
+ [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; }
+
+ void setResolving(bool resolving, int resolutionTicket)
+ {
+ m_is_resolving = resolving;
+ m_resolution_ticket = resolutionTicket;
+ }
+
+ // Delete all files of this resource.
+ bool destroy();
+
+ protected:
+ /* The file corresponding to this resource. */
+ QFileInfo m_file_info;
+ /* The cached date when this file was last changed. */
+ QDateTime m_changed_date_time;
+
+ /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */
+ QString m_internal_id;
+ /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */
+ QString m_name;
+
+ /* The type of file we're dealing with. */
+ ResourceType m_type = ResourceType::UNKNOWN;
+
+ /* Whether the resource is enabled (e.g. shows up in the game) or not. */
+ bool m_enabled = true;
+
+ /* Used to keep trach of pending / concluded actions on the resource. */
+ bool m_is_resolving = false;
+ bool m_is_resolved = false;
+ int m_resolution_ticket = 0;
+};
diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
new file mode 100644
index 00000000..45d1db59
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -0,0 +1,524 @@
+#include "ResourceFolderModel.h"
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QMimeData>
+#include <QThreadPool>
+#include <QUrl>
+
+#include "FileSystem.h"
+
+#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
+
+#include "tasks/Task.h"
+
+ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
+{
+ m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
+ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
+
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
+}
+
+ResourceFolderModel::~ResourceFolderModel()
+{
+ while (!QThreadPool::globalInstance()->waitForDone(100))
+ QCoreApplication::processEvents();
+}
+
+bool ResourceFolderModel::startWatching(const QStringList paths)
+{
+ if (m_is_watching)
+ return false;
+
+ auto couldnt_be_watched = m_watcher.addPaths(paths);
+ for (auto path : paths) {
+ if (couldnt_be_watched.contains(path))
+ qDebug() << "Failed to start watching " << path;
+ else
+ qDebug() << "Started watching " << path;
+ }
+
+ update();
+
+ m_is_watching = !m_is_watching;
+ return m_is_watching;
+}
+
+bool ResourceFolderModel::stopWatching(const QStringList paths)
+{
+ if (!m_is_watching)
+ return false;
+
+ auto couldnt_be_stopped = m_watcher.removePaths(paths);
+ for (auto path : paths) {
+ if (couldnt_be_stopped.contains(path))
+ qDebug() << "Failed to stop watching " << path;
+ else
+ qDebug() << "Stopped watching " << path;
+ }
+
+ m_is_watching = !m_is_watching;
+ return !m_is_watching;
+}
+
+bool ResourceFolderModel::installResource(QString original_path)
+{
+ if (!m_can_interact) {
+ return false;
+ }
+
+ // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
+ original_path = FS::NormalizePath(original_path);
+ QFileInfo file_info(original_path);
+
+ if (!file_info.exists() || !file_info.isReadable()) {
+ qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path;
+ return false;
+ }
+ qDebug() << "Installing: " << file_info.absoluteFilePath();
+
+ Resource resource(file_info);
+ if (!resource.valid()) {
+ qWarning() << original_path << "is not a valid resource. Ignoring it.";
+ return false;
+ }
+
+ auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName()));
+ if (original_path == new_path) {
+ qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense...";
+ return false;
+ }
+
+ switch (resource.type()) {
+ case ResourceType::SINGLEFILE:
+ case ResourceType::ZIPFILE:
+ case ResourceType::LITEMOD: {
+ if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
+ if (!QFile::remove(new_path)) {
+ qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
+ return false;
+ }
+ qDebug() << new_path << "has been deleted.";
+ }
+
+ if (!QFile::copy(original_path, new_path)) {
+ qCritical() << "Copy from" << original_path << "to" << new_path << "has failed.";
+ return false;
+ }
+
+ FS::updateTimestamp(new_path);
+
+ QFileInfo new_path_file_info(new_path);
+ resource.setFile(new_path_file_info);
+
+ if (!m_is_watching)
+ return update();
+
+ return true;
+ }
+ case ResourceType::FOLDER: {
+ if (QFile::exists(new_path)) {
+ qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path;
+ return false;
+ }
+
+ if (!FS::copy(original_path, new_path)()) {
+ qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed.";
+ return false;
+ }
+
+ QFileInfo newpathInfo(new_path);
+ resource.setFile(newpathInfo);
+
+ if (!m_is_watching)
+ return update();
+
+ return true;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+bool ResourceFolderModel::uninstallResource(QString file_name)
+{
+ for (auto& resource : m_resources) {
+ if (resource->fileinfo().fileName() == file_name) {
+ auto res = resource->destroy();
+
+ update();
+
+ return res;
+ }
+ }
+ return false;
+}
+
+bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
+{
+ if (!m_can_interact)
+ return false;
+
+ if (indexes.isEmpty())
+ return true;
+
+ for (auto i : indexes) {
+ if (i.column() != 0) {
+ continue;
+ }
+
+ auto& resource = m_resources.at(i.row());
+
+ resource->destroy();
+ }
+
+ update();
+
+ return true;
+}
+
+bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
+{
+ if (!m_can_interact)
+ return false;
+
+ if (indexes.isEmpty())
+ return true;
+
+ bool succeeded = true;
+ for (auto const& idx : indexes) {
+ if (!validateIndex(idx) || idx.column() != 0)
+ continue;
+
+ int row = idx.row();
+
+ auto& resource = m_resources[row];
+
+ // Preserve the row, but change its ID
+ auto old_id = resource->internal_id();
+ if (!resource->enable(action)) {
+ succeeded = false;
+ continue;
+ }
+
+ auto new_id = resource->internal_id();
+ if (m_resources_index.contains(new_id)) {
+ // FIXME: https://github.com/PolyMC/PolyMC/issues/550
+ }
+
+ m_resources_index.remove(old_id);
+ m_resources_index[new_id] = row;
+
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+
+ return succeeded;
+}
+
+static QMutex s_update_task_mutex;
+bool ResourceFolderModel::update()
+{
+ // We hold a lock here to prevent race conditions on the m_current_update_task reset.
+ QMutexLocker lock(&s_update_task_mutex);
+
+ // Already updating, so we schedule a future update and return.
+ if (m_current_update_task) {
+ m_scheduled_update = true;
+ return false;
+ }
+
+ m_current_update_task.reset(createUpdateTask());
+ if (!m_current_update_task)
+ return false;
+
+ connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
+ Qt::ConnectionType::QueuedConnection);
+ connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
+ connect(m_current_update_task.get(), &Task::finished, this, [=] {
+ m_current_update_task.reset();
+ if (m_scheduled_update) {
+ m_scheduled_update = false;
+ update();
+ } else {
+ emit updateFinished();
+ }
+ }, Qt::ConnectionType::QueuedConnection);
+
+ QThreadPool::globalInstance()->start(m_current_update_task.get());
+
+ return true;
+}
+
+void ResourceFolderModel::resolveResource(Resource::Ptr res)
+{
+ if (!res->shouldResolve()) {
+ return;
+ }
+
+ auto task = createParseTask(*res);
+ if (!task)
+ return;
+
+ int ticket = m_next_resolution_ticket.fetch_add(1);
+
+ res->setResolving(true, ticket);
+ m_active_parse_tasks.insert(ticket, task);
+
+ connect(
+ task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
+ connect(
+ task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
+ connect(
+ task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
+
+ QThreadPool::globalInstance()->start(task);
+}
+
+void ResourceFolderModel::onUpdateSucceeded()
+{
+ auto update_results = static_cast<BasicFolderLoadTask*>(m_current_update_task.get())->result();
+
+ auto& new_resources = update_results->resources;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ auto current_list = m_resources_index.keys();
+ QSet<QString> current_set(current_list.begin(), current_list.end());
+
+ auto new_list = new_resources.keys();
+ QSet<QString> new_set(new_list.begin(), new_list.end());
+#else
+ QSet<QString> current_set(m_resources_index.keys().toSet());
+ QSet<QString> new_set(new_resources.keys().toSet());
+#endif
+
+ applyUpdates(current_set, new_set, new_resources);
+}
+
+void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
+{
+ auto iter = m_active_parse_tasks.constFind(ticket);
+ if (iter == m_active_parse_tasks.constEnd())
+ return;
+
+ int row = m_resources_index[resource_id];
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
+}
+
+Task* ResourceFolderModel::createUpdateTask()
+{
+ return new BasicFolderLoadTask(m_dir);
+}
+
+bool ResourceFolderModel::hasPendingParseTasks() const
+{
+ return !m_active_parse_tasks.isEmpty();
+}
+
+void ResourceFolderModel::directoryChanged(QString path)
+{
+ update();
+}
+
+Qt::DropActions ResourceFolderModel::supportedDropActions() const
+{
+ // copy from outside, move from within and other resource lists
+ return Qt::CopyAction | Qt::MoveAction;
+}
+
+Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ auto flags = defaultFlags;
+ if (!m_can_interact) {
+ flags &= ~Qt::ItemIsDropEnabled;
+ } else {
+ flags |= Qt::ItemIsDropEnabled;
+ if (index.isValid()) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ }
+ return flags;
+}
+
+QStringList ResourceFolderModel::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+
+bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
+{
+ if (action == Qt::IgnoreAction) {
+ return true;
+ }
+
+ // check if the action is supported
+ if (!data || !(action & supportedDropActions())) {
+ return false;
+ }
+
+ // files dropped from outside?
+ if (data->hasUrls()) {
+ auto urls = data->urls();
+ for (auto url : urls) {
+ // only local files may be dropped...
+ if (!url.isLocalFile()) {
+ continue;
+ }
+ // TODO: implement not only copy, but also move
+ // FIXME: handle errors here
+ installResource(url.toLocalFile());
+ }
+ return true;
+ }
+ return false;
+}
+
+bool ResourceFolderModel::validateIndex(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return false;
+
+ int row = index.row();
+ if (row < 0 || row >= m_resources.size())
+ return false;
+
+ return true;
+}
+
+QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
+{
+ if (!validateIndex(index))
+ return {};
+
+ int row = index.row();
+ int column = index.column();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (column) {
+ case NAME_COLUMN:
+ return m_resources[row]->name();
+ case DATE_COLUMN:
+ return m_resources[row]->dateTimeChanged();
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole:
+ return m_resources[row]->internal_id();
+ case Qt::CheckStateRole:
+ switch (column) {
+ case ACTIVE_COLUMN:
+ return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return {};
+ }
+ default:
+ return {};
+ }
+}
+
+bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ int row = index.row();
+ if (row < 0 || row >= rowCount(index) || !index.isValid())
+ return false;
+
+ if (role == Qt::CheckStateRole)
+ return setResourceEnabled({ index }, EnableAction::TOGGLE);
+
+ return false;
+}
+
+QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case NAME_COLUMN:
+ return tr("Name");
+ case DATE_COLUMN:
+ return tr("Last modified");
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole: {
+ switch (section) {
+ case ACTIVE_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("Is the resource enabled?");
+ case NAME_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The name of the resource.");
+ case DATE_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The date and time this resource was last changed (or added).");
+ default:
+ return {};
+ }
+ }
+ default:
+ break;
+ }
+
+ return {};
+}
+
+QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
+{
+ return new ProxyModel(parent);
+}
+
+SortType ResourceFolderModel::columnToSortKey(size_t column) const
+{
+ Q_ASSERT(m_column_sort_keys.size() == columnCount());
+ return m_column_sort_keys.at(column);
+}
+
+void ResourceFolderModel::enableInteraction(bool enabled)
+{
+ if (m_can_interact == enabled)
+ return;
+
+ m_can_interact = enabled;
+ if (size())
+ emit dataChanged(index(0), index(size() - 1));
+}
+
+/* Standard Proxy Model for createFilterProxyModel */
+[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
+{
+ auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
+ if (!model)
+ return true;
+
+ const auto& resource = model->at(source_row);
+
+ return resource.applyFilter(filterRegularExpression());
+}
+
+[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
+{
+ auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
+ if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+
+ // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
+ // proceed.
+
+ auto column_sort_key = model->columnToSortKey(source_left.column());
+ auto const& resource_left = model->at(source_left.row());
+ auto const& resource_right = model->at(source_right.row());
+
+ auto compare_result = resource_left.compare(resource_right, column_sort_key);
+ if (compare_result.first == 0)
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+
+ if (compare_result.second || sortOrder() != Qt::DescendingOrder)
+ return (compare_result.first < 0);
+ return (compare_result.first > 0);
+}
diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h
new file mode 100644
index 00000000..5652c156
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel.h
@@ -0,0 +1,332 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QDir>
+#include <QFileSystemWatcher>
+#include <QMutex>
+#include <QSet>
+#include <QSortFilterProxyModel>
+
+#include "Resource.h"
+
+#include "tasks/Task.h"
+
+class QSortFilterProxyModel;
+
+/** A basic model for external resources.
+ *
+ * This model manages a list of resources. As such, external users of such resources do not own them,
+ * and the resource's lifetime is contingent on the model's lifetime.
+ *
+ * TODO: Make the resources unique pointers accessible through weak pointers.
+ */
+class ResourceFolderModel : public QAbstractListModel {
+ Q_OBJECT
+ public:
+ ResourceFolderModel(QDir, QObject* parent = nullptr);
+ ~ResourceFolderModel() override;
+
+ /** Starts watching the paths for changes.
+ *
+ * Returns whether starting to watch all the paths was successful.
+ * If one or more fails, it returns false.
+ */
+ bool startWatching(const QStringList paths);
+
+ /** Stops watching the paths for changes.
+ *
+ * Returns whether stopping to watch all the paths was successful.
+ * If one or more fails, it returns false.
+ */
+ bool stopWatching(const QStringList paths);
+
+ /* Helper methods for subclasses, using a predetermined list of paths. */
+ virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); };
+ virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); };
+
+ /** Given a path in the system, install that resource, moving it to its place in the
+ * instance file hierarchy.
+ *
+ * Returns whether the installation was succcessful.
+ */
+ virtual bool installResource(QString path);
+
+ /** Uninstall (i.e. remove all data about it) a resource, given its file name.
+ *
+ * Returns whether the removal was successful.
+ */
+ virtual bool uninstallResource(QString file_name);
+ virtual bool deleteResources(const QModelIndexList&);
+
+ /** Applies the given 'action' to the resources in 'indexes'.
+ *
+ * Returns whether the action was successfully applied to all resources.
+ */
+ virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action);
+
+ /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */
+ virtual bool update();
+
+ /** Creates a new parse task, if needed, for 'res' and start it.*/
+ virtual void resolveResource(Resource::Ptr res);
+
+ [[nodiscard]] size_t size() const { return m_resources.size(); };
+ [[nodiscard]] bool empty() const { return size() == 0; }
+ [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); }
+ [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); }
+ [[nodiscard]] QList<Resource::Ptr> const& all() const { return m_resources; }
+
+ [[nodiscard]] QDir const& dir() const { return m_dir; }
+
+ /** Checks whether there's any parse tasks being done.
+ *
+ * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having
+ * such tasks would introduce an undefined behavior, most likely resulting in a crash.
+ */
+ [[nodiscard]] bool hasPendingParseTasks() const;
+
+ /* Qt behavior */
+
+ /* Basic columns */
+ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
+
+ [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); }
+ [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; };
+
+ [[nodiscard]] Qt::DropActions supportedDropActions() const override;
+
+ /// flags, mostly to support drag&drop
+ [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override;
+ [[nodiscard]] QStringList mimeTypes() const override;
+ bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
+
+ [[nodiscard]] bool validateIndex(const QModelIndex& index) const;
+
+ [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
+
+ [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+
+ /** This creates a proxy model to filter / sort the model for a UI.
+ *
+ * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
+ */
+ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
+
+ [[nodiscard]] SortType columnToSortKey(size_t column) const;
+
+ class ProxyModel : public QSortFilterProxyModel {
+ public:
+ explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
+
+ protected:
+ [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
+ [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
+ };
+
+ public slots:
+ void enableInteraction(bool enabled);
+ void disableInteraction(bool disabled) { enableInteraction(!disabled); }
+
+ signals:
+ void updateFinished();
+
+ protected:
+ /** This creates a new update task to be executed by update().
+ *
+ * The task should load and parse all resources necessary, and provide a way of accessing such results.
+ *
+ * This Task is normally executed when opening a page, so it shouldn't contain much heavy work.
+ * If such work is needed, try using it in the Task create by createParseTask() instead!
+ */
+ [[nodiscard]] virtual Task* createUpdateTask();
+
+ /** This creates a new parse task to be executed by onUpdateSucceeded().
+ *
+ * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
+ * in the background, so it slowly updates the UI as tasks get done.
+ */
+ [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; };
+
+ /** Standard implementation of the model update logic.
+ *
+ * It uses set operations to find differences between the current state and the updated state,
+ * to act only on those disparities.
+ *
+ * The implementation is at the end of this header.
+ */
+ template <typename T>
+ void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources);
+
+ protected slots:
+ void directoryChanged(QString);
+
+ /** Called when the update task is successful.
+ *
+ * This usually calls static_cast on the specific Task type returned by createUpdateTask,
+ * so care must be taken in such cases.
+ * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that).
+ */
+ virtual void onUpdateSucceeded();
+ virtual void onUpdateFailed() {}
+
+ /** Called when the parse task with the given ticket is successful.
+ *
+ * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass
+ * if the resource is complex and has more stuff to parse.
+ */
+ virtual void onParseSucceeded(int ticket, QString resource_id);
+ virtual void onParseFailed(int ticket, QString resource_id) {}
+
+ protected:
+ // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
+ // As such, the order in with they appear is very important!
+ QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
+
+ bool m_can_interact = true;
+
+ QDir m_dir;
+ QFileSystemWatcher m_watcher;
+ bool m_is_watching = false;
+
+ Task::Ptr m_current_update_task = nullptr;
+ bool m_scheduled_update = false;
+
+ QList<Resource::Ptr> m_resources;
+
+ // Represents the relationship between a resource's internal ID and it's row position on the model.
+ QMap<QString, int> m_resources_index;
+
+ QMap<int, Task::Ptr> m_active_parse_tasks;
+ std::atomic<int> m_next_resolution_ticket = 0;
+};
+
+/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
+#define RESOURCE_HELPERS(T) \
+ [[nodiscard]] T* operator[](size_t index) \
+ { \
+ return static_cast<T*>(m_resources[index].get()); \
+ } \
+ [[nodiscard]] T* at(size_t index) \
+ { \
+ return static_cast<T*>(m_resources[index].get()); \
+ } \
+ [[nodiscard]] const T* at(size_t index) const \
+ { \
+ return static_cast<const T*>(m_resources.at(index).get()); \
+ } \
+ [[nodiscard]] T* first() \
+ { \
+ return static_cast<T*>(m_resources.first().get()); \
+ } \
+ [[nodiscard]] T* last() \
+ { \
+ return static_cast<T*>(m_resources.last().get()); \
+ } \
+ [[nodiscard]] T* find(QString id) \
+ { \
+ auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \
+ [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \
+ if (iter == m_resources.constEnd()) \
+ return nullptr; \
+ return static_cast<T*>((*iter).get()); \
+ }
+
+/* Template definition to avoid some code duplication */
+template <typename T>
+void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources)
+{
+ // see if the kept resources changed in some way
+ {
+ QSet<QString> kept_set = current_set;
+ kept_set.intersect(new_set);
+
+ for (auto const& kept : kept_set) {
+ auto row_it = m_resources_index.constFind(kept);
+ Q_ASSERT(row_it != m_resources_index.constEnd());
+ auto row = row_it.value();
+
+ auto& new_resource = new_resources[kept];
+ auto const& current_resource = m_resources[row];
+
+ if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
+ // no significant change, ignore...
+ continue;
+ }
+
+ // If the resource is resolving, but something about it changed, we don't want to
+ // continue the resolving.
+ if (current_resource->isResolving()) {
+ auto ticket = current_resource->resolutionTicket();
+ if (m_active_parse_tasks.contains(ticket)) {
+ auto task = (*m_active_parse_tasks.find(ticket)).get();
+ task->abort();
+ }
+ }
+
+ m_resources[row].reset(new_resource);
+ resolveResource(m_resources.at(row));
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+ }
+
+ // remove resources no longer present
+ {
+ QSet<QString> removed_set = current_set;
+ removed_set.subtract(new_set);
+
+ QList<int> removed_rows;
+ for (auto& removed : removed_set)
+ removed_rows.append(m_resources_index[removed]);
+
+ std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>());
+
+ for (auto& removed_index : removed_rows) {
+ auto removed_it = m_resources.begin() + removed_index;
+
+ Q_ASSERT(removed_it != m_resources.end());
+ Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
+
+ if ((*removed_it)->isResolving()) {
+ auto ticket = (*removed_it)->resolutionTicket();
+ if (m_active_parse_tasks.contains(ticket)) {
+ auto task = (*m_active_parse_tasks.find(ticket)).get();
+ task->abort();
+ }
+ }
+
+ beginRemoveRows(QModelIndex(), removed_index, removed_index);
+ m_resources.erase(removed_it);
+ endRemoveRows();
+ }
+ }
+
+ // add new resources to the end
+ {
+ QSet<QString> added_set = new_set;
+ added_set.subtract(current_set);
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (added_set.size() > 0) {
+ beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
+
+ for (auto& added : added_set) {
+ auto res = new_resources[added];
+ m_resources.append(res);
+ resolveResource(res);
+ }
+
+ endInsertRows();
+ }
+ }
+
+ // update index
+ {
+ m_resources_index.clear();
+ int idx = 0;
+ for (auto const& mod : m_resources) {
+ m_resources_index[mod->internal_id()] = idx;
+ idx++;
+ }
+ }
+}
diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp
new file mode 100644
index 00000000..3fc10a2f
--- /dev/null
+++ b/launcher/minecraft/mod/ResourcePack.cpp
@@ -0,0 +1,116 @@
+#include "ResourcePack.h"
+
+#include <QDebug>
+#include <QMap>
+#include <QRegularExpression>
+
+#include "Version.h"
+
+#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
+
+// Values taken from:
+// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
+static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
+ { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
+ { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
+ { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
+ { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
+ { 9, { Version("1.19"), Version("1.19.2") } },
+};
+
+void ResourcePack::setPackFormat(int new_format_id)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ if (!s_pack_format_versions.contains(new_format_id)) {
+ qWarning() << "Pack format '%1' is not a recognized resource pack id!";
+ }
+
+ m_pack_format = new_format_id;
+}
+
+void ResourcePack::setDescription(QString new_description)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ m_description = new_description;
+}
+
+void ResourcePack::setImage(QImage new_image)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ Q_ASSERT(!new_image.isNull());
+
+ if (m_pack_image_cache_key.key.isValid())
+ QPixmapCache::remove(m_pack_image_cache_key.key);
+
+ m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image));
+ m_pack_image_cache_key.was_ever_used = true;
+}
+
+QPixmap ResourcePack::image(QSize size)
+{
+ QPixmap cached_image;
+ if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
+ if (size.isNull())
+ return cached_image;
+ return cached_image.scaled(size);
+ }
+
+ // No valid image we can get
+ if (!m_pack_image_cache_key.was_ever_used)
+ return {};
+
+ // Imaged got evicted from the cache. Re-process it and retry.
+ ResourcePackUtils::process(*this);
+ return image(size);
+}
+
+std::pair<Version, Version> ResourcePack::compatibleVersions() const
+{
+ if (!s_pack_format_versions.contains(m_pack_format)) {
+ return { {}, {} };
+ }
+
+ return s_pack_format_versions.constFind(m_pack_format).value();
+}
+
+std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type) const
+{
+ auto const& cast_other = static_cast<ResourcePack const&>(other);
+
+ switch (type) {
+ default: {
+ auto res = Resource::compare(other, type);
+ if (res.first != 0)
+ return res;
+ }
+ case SortType::PACK_FORMAT: {
+ auto this_ver = packFormat();
+ auto other_ver = cast_other.packFormat();
+
+ if (this_ver > other_ver)
+ return { 1, type == SortType::PACK_FORMAT };
+ if (this_ver < other_ver)
+ return { -1, type == SortType::PACK_FORMAT };
+ }
+ }
+ return { 0, false };
+}
+
+bool ResourcePack::applyFilter(QRegularExpression filter) const
+{
+ if (filter.match(description()).hasMatch())
+ return true;
+
+ if (filter.match(QString::number(packFormat())).hasMatch())
+ return true;
+
+ if (filter.match(compatibleVersions().first.toString()).hasMatch())
+ return true;
+ if (filter.match(compatibleVersions().second.toString()).hasMatch())
+ return true;
+
+ return Resource::applyFilter(filter);
+}
diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h
new file mode 100644
index 00000000..03121908
--- /dev/null
+++ b/launcher/minecraft/mod/ResourcePack.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "Resource.h"
+
+#include <QImage>
+#include <QMutex>
+#include <QPixmap>
+#include <QPixmapCache>
+
+class Version;
+
+/* TODO:
+ *
+ * Store localized descriptions
+ * */
+
+class ResourcePack : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+
+ ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
+ ResourcePack(QFileInfo file_info) : Resource(file_info) {}
+
+ /** Gets the numerical ID of the pack format. */
+ [[nodiscard]] int packFormat() const { return m_pack_format; }
+ /** Gets, respectively, the lower and upper versions supported by the set pack format. */
+ [[nodiscard]] std::pair<Version, Version> compatibleVersions() const;
+
+ /** Gets the description of the resource pack. */
+ [[nodiscard]] QString description() const { return m_description; }
+
+ /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
+ [[nodiscard]] QPixmap image(QSize size);
+
+ /** Thread-safe. */
+ void setPackFormat(int new_format_id);
+
+ /** Thread-safe. */
+ void setDescription(QString new_description);
+
+ /** Thread-safe. */
+ void setImage(QImage new_image);
+
+ [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
+ [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
+
+ protected:
+ mutable QMutex m_data_lock;
+
+ /* The 'version' of a resource pack, as defined in the pack.mcmeta file.
+ * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
+ */
+ int m_pack_format = 0;
+
+ /** The resource pack's description, as defined in the pack.mcmeta file.
+ */
+ QString m_description;
+
+ /** The resource pack's image file cache key, for access in the QPixmapCache global instance.
+ *
+ * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true),
+ * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache.
+ */
+ struct {
+ QPixmapCache::Key key;
+ bool was_ever_used = false;
+ } m_pack_image_cache_key;
+};
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
index 276804ed..f8a6c1cf 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -1,58 +1,151 @@
// 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/>.
-*
-* This file incorporates work covered by the following copyright and
-* permission notice:
-*
-* Copyright 2013-2021 MultiMC Contributors
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
#include "ResourcePackFolderModel.h"
-ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
+#include "Version.h"
+
+#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
+#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
+
+ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir))
+{
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
}
-QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
- if (role == Qt::ToolTipRole) {
- switch (section) {
- case ActiveColumn:
- return tr("Is the resource pack enabled?");
- case NameColumn:
- return tr("The name of the resource pack.");
- case VersionColumn:
- return tr("The version of the resource pack.");
- case DateColumn:
- return tr("The date and time this resource pack was last changed (or added).");
- default:
- return QVariant();
+QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
+{
+ if (!validateIndex(index))
+ return {};
+
+ int row = index.row();
+ int column = index.column();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (column) {
+ case NameColumn:
+ return m_resources[row]->name();
+ case PackFormatColumn: {
+ auto resource = at(row);
+ auto pack_format = resource->packFormat();
+ if (pack_format == 0)
+ return tr("Unrecognized");
+
+ auto version_bounds = resource->compatibleVersions();
+ if (version_bounds.first.toString().isEmpty())
+ return QString::number(pack_format);
+
+ return QString("%1 (%2 - %3)")
+ .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString());
+ }
+ case DateColumn:
+ return m_resources[row]->dateTimeChanged();
+
+ default:
+ return {};
+ }
+
+ case Qt::ToolTipRole: {
+ if (column == PackFormatColumn) {
+ //: The string being explained by this is in the format: ID (Lower version - Upper version)
+ return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
+ }
+ return m_resources[row]->internal_id();
}
+ case Qt::CheckStateRole:
+ switch (column) {
+ case ActiveColumn:
+ return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return {};
+ }
+ default:
+ return {};
}
+}
+
+QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case ActiveColumn:
+ return QString();
+ case NameColumn:
+ return tr("Name");
+ case PackFormatColumn:
+ return tr("Pack Format");
+ case DateColumn:
+ return tr("Last changed");
+ default:
+ return {};
+ }
+
+ case Qt::ToolTipRole:
+ switch (section) {
+ case ActiveColumn:
+ return tr("Is the resource pack enabled? (Only valid for ZIPs)");
+ case NameColumn:
+ return tr("The name of the resource pack.");
+ case PackFormatColumn:
+ //: The string being explained by this is in the format: ID (Lower version - Upper version)
+ return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
+ case DateColumn:
+ return tr("The date and time this resource pack was last changed (or added).");
+ default:
+ return {};
+ }
+ default:
+ return {};
+ }
+ return {};
+}
+
+int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
+{
+ return NUM_COLUMNS;
+}
+
+Task* ResourcePackFolderModel::createUpdateTask()
+{
+ return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); });
+}
- return ModFolderModel::headerData(section, orientation, role);
+Task* ResourcePackFolderModel::createParseTask(Resource& resource)
+{
+ return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource));
}
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h
index 0cd6214b..cb620ce2 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.h
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.h
@@ -1,13 +1,31 @@
#pragma once
-#include "ModFolderModel.h"
+#include "ResourceFolderModel.h"
-class ResourcePackFolderModel : public ModFolderModel
+#include "ResourcePack.h"
+
+class ResourcePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
-
public:
+ enum Columns
+ {
+ ActiveColumn = 0,
+ NameColumn,
+ PackFormatColumn,
+ DateColumn,
+ NUM_COLUMNS
+ };
+
explicit ResourcePackFolderModel(const QString &dir);
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+ [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ [[nodiscard]] int columnCount(const QModelIndex &parent) const override;
+
+ [[nodiscard]] Task* createUpdateTask() override;
+ [[nodiscard]] Task* createParseTask(Resource&) override;
+
+ RESOURCE_HELPERS(ResourcePack)
};
diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h
new file mode 100644
index 00000000..a3aa958f
--- /dev/null
+++ b/launcher/minecraft/mod/ShaderPackFolderModel.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "ResourceFolderModel.h"
+
+class ShaderPackFolderModel : public ResourceFolderModel {
+ Q_OBJECT
+
+ public:
+ explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
+};
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
index e3a22219..2c7c945b 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -35,24 +35,4 @@
#include "TexturePackFolderModel.h"
-TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
-}
-
-QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
- if (role == Qt::ToolTipRole) {
- switch (section) {
- case ActiveColumn:
- return tr("Is the texture pack enabled?");
- case NameColumn:
- return tr("The name of the texture pack.");
- case VersionColumn:
- return tr("The version of the texture pack.");
- case DateColumn:
- return tr("The date and time this texture pack was last changed (or added).");
- default:
- return QVariant();
- }
- }
-
- return ModFolderModel::headerData(section, orientation, role);
-}
+TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h
index a59d5119..69e98661 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.h
+++ b/launcher/minecraft/mod/TexturePackFolderModel.h
@@ -1,13 +1,11 @@
#pragma once
-#include "ModFolderModel.h"
+#include "ResourceFolderModel.h"
-class TexturePackFolderModel : public ModFolderModel
+class TexturePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
explicit TexturePackFolderModel(const QString &dir);
-
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
};
diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
new file mode 100644
index 00000000..be0e752d
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include <QDir>
+#include <QMap>
+#include <QObject>
+
+#include <memory>
+
+#include "minecraft/mod/Resource.h"
+
+#include "tasks/Task.h"
+
+/** Very simple task that just loads a folder's contents directly.
+ */
+class BasicFolderLoadTask : public Task {
+ Q_OBJECT
+ public:
+ struct Result {
+ QMap<QString, Resource::Ptr> resources;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+
+ [[nodiscard]] ResultPtr result() const { return m_result; }
+
+ public:
+ BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result)
+ {
+ m_create_func = [](QFileInfo const& entry) -> Resource* {
+ return new Resource(entry);
+ };
+ }
+ BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function)
+ : Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function))
+ {}
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override
+ {
+ m_aborted.store(true);
+ return true;
+ }
+
+ void executeTask() override
+ {
+ m_dir.refresh();
+ for (auto entry : m_dir.entryInfoList()) {
+ auto resource = m_create_func(entry);
+ m_result->resources.insert(resource->internal_id(), resource);
+ }
+
+ if (m_aborted)
+ emit finished();
+ else
+ emitSucceeded();
+ }
+
+private:
+ QDir m_dir;
+ ResultPtr m_result;
+
+ std::atomic<bool> m_aborted = false;
+
+ std::function<Resource*(QFileInfo const&)> m_create_func;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
index 1519f49d..8a6e54d8 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
@@ -20,22 +20,22 @@ namespace {
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
-std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
+ModDetails ReadMCModInfo(QByteArray contents)
{
- auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
+ auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails
{
if (!arr.at(0).isObject()) {
- return nullptr;
+ return {};
}
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
auto firstObj = arr.at(0).toObject();
- details->mod_id = firstObj.value("modid").toString();
+ details.mod_id = firstObj.value("modid").toString();
auto name = firstObj.value("name").toString();
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
if(name != "Example Mod") {
- details->name = name;
+ details.name = name;
}
- details->version = firstObj.value("version").toString();
+ details.version = firstObj.value("version").toString();
auto homeurl = firstObj.value("url").toString().trimmed();
if(!homeurl.isEmpty())
{
@@ -45,8 +45,8 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
homeurl.prepend("http://");
}
}
- details->homeurl = homeurl;
- details->description = firstObj.value("description").toString();
+ details.homeurl = homeurl;
+ details.description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0) {
// FIXME: what is the format of this? is there any?
@@ -55,7 +55,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
for (auto author: authors)
{
- details->authors.append(author.toString());
+ details.authors.append(author.toString());
}
return details;
};
@@ -83,7 +83,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
{
qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents;
- return nullptr;
+ return {};
}
auto arrVal = jsonDoc.object().value("modlist");
if(arrVal.isUndefined()) {
@@ -94,13 +94,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
return getInfoFromArray(arrVal.toArray());
}
}
- return nullptr;
+ return {};
}
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
-std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
+ModDetails ReadMCModTOML(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
char errbuf[200];
// top-level table
@@ -108,7 +108,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlData)
{
- return nullptr;
+ return {};
}
// array defined by [[mods]]
@@ -116,7 +116,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsArr)
{
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
- return nullptr;
+ return {};
}
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
@@ -124,33 +124,33 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsTable0)
{
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
- return nullptr;
+ return {};
}
// mandatory properties - always in [[mods]]
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
if(modIdDatum.ok)
{
- details->mod_id = modIdDatum.u.s;
+ details.mod_id = modIdDatum.u.s;
// library says this is required for strings
free(modIdDatum.u.s);
}
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
if(versionDatum.ok)
{
- details->version = versionDatum.u.s;
+ details.version = versionDatum.u.s;
free(versionDatum.u.s);
}
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
if(displayNameDatum.ok)
{
- details->name = displayNameDatum.u.s;
+ details.name = displayNameDatum.u.s;
free(displayNameDatum.u.s);
}
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
if(descriptionDatum.ok)
{
- details->description = descriptionDatum.u.s;
+ details.description = descriptionDatum.u.s;
free(descriptionDatum.u.s);
}
@@ -173,7 +173,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
if(!authors.isEmpty())
{
- details->authors.append(authors);
+ details.authors.append(authors);
}
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
@@ -200,7 +200,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
homeurl.prepend("http://");
}
}
- details->homeurl = homeurl;
+ details.homeurl = homeurl;
// this seems to be recursive, so it should free everything
toml_free(tomlData);
@@ -209,20 +209,20 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
// https://fabricmc.net/wiki/documentation:fabric_mod_json
-std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
+ModDetails ReadFabricModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
- details->mod_id = object.value("id").toString();
- details->version = object.value("version").toString();
+ details.mod_id = object.value("id").toString();
+ details.version = object.value("version").toString();
- details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
- details->description = object.value("description").toString();
+ details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
+ details.description = object.value("description").toString();
if (schemaVersion >= 1)
{
@@ -230,10 +230,10 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
for (auto author: authors)
{
if(author.isObject()) {
- details->authors.append(author.toObject().value("name").toString());
+ details.authors.append(author.toObject().value("name").toString());
}
else {
- details->authors.append(author.toString());
+ details.authors.append(author.toString());
}
}
@@ -243,7 +243,7 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
if (contact.contains("homepage"))
{
- details->homeurl = contact.value("homepage").toString();
+ details.homeurl = contact.value("homepage").toString();
}
}
}
@@ -251,50 +251,50 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
}
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
-std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
+ModDetails ReadQuiltModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1)
{
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
- details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
- details->version = Json::requireString(modInfo.value("version"), "Mod version");
+ details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
+ details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
- details->name = Json::ensureString(modMetadata.value("name"), details->mod_id);
- details->description = Json::ensureString(modMetadata.value("description"));
+ details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
+ details.description = Json::ensureString(modMetadata.value("description"));
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
// We don't really care about the role of a contributor here
- details->authors += modContributors.keys();
+ details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact"));
if (modContact.contains("homepage"))
{
- details->homeurl = Json::requireString(modContact.value("homepage"));
+ details.homeurl = Json::requireString(modContact.value("homepage"));
}
}
return details;
}
-std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
+ModDetails ReadForgeInfo(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
// Read the data
- details->name = "Minecraft Forge";
- details->mod_id = "Forge";
- details->homeurl = "http://www.minecraftforge.net/forum/";
+ details.name = "Minecraft Forge";
+ details.mod_id = "Forge";
+ details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
return details;
@@ -304,47 +304,47 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.number", "0").toString();
- details->version = major + "." + minor + "." + revision + "." + build;
+ details.version = major + "." + minor + "." + revision + "." + build;
return details;
}
-std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
+ModDetails ReadLiteModInfo(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if (object.contains("name"))
{
- details->mod_id = details->name = object.value("name").toString();
+ details.mod_id = details.name = object.value("name").toString();
}
if (object.contains("version"))
{
- details->version = object.value("version").toString("");
+ details.version = object.value("version").toString("");
}
else
{
- details->version = object.value("revision").toString("");
+ details.version = object.value("revision").toString("");
}
- details->mcversion = object.value("mcversion").toString();
+ details.mcversion = object.value("mcversion").toString();
auto author = object.value("author").toString();
if(!author.isEmpty()) {
- details->authors.append(author);
+ details.authors.append(author);
}
- details->description = object.value("description").toString();
- details->homeurl = object.value("url").toString();
+ details.description = object.value("description").toString();
+ details.homeurl = object.value("url").toString();
return details;
}
}
-LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
+LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile):
+ Task(nullptr, false),
m_token(token),
m_type(type),
m_modFile(modFile),
m_result(new Result())
-{
-}
+{}
void LocalModParseTask::processAsZip()
{
@@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip()
file.close();
// to replace ${file.jarVersion} with the actual version, as needed
- if (m_result->details && m_result->details->version == "${file.jarVersion}")
+ if (m_result->details.version == "${file.jarVersion}")
{
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
{
@@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip()
manifestVersion = "NONE";
}
- m_result->details->version = manifestVersion;
+ m_result->details.version = manifestVersion;
file.close();
}
@@ -497,21 +497,31 @@ void LocalModParseTask::processAsLitemod()
zip.close();
}
-void LocalModParseTask::run()
+bool LocalModParseTask::abort()
+{
+ m_aborted.store(true);
+ return true;
+}
+
+void LocalModParseTask::executeTask()
{
switch(m_type)
{
- case Mod::MOD_ZIPFILE:
+ case ResourceType::ZIPFILE:
processAsZip();
break;
- case Mod::MOD_FOLDER:
+ case ResourceType::FOLDER:
processAsFolder();
break;
- case Mod::MOD_LITEMOD:
+ case ResourceType::LITEMOD:
processAsLitemod();
break;
default:
break;
}
- emit finished(m_token);
+
+ if (m_aborted)
+ emit finished();
+ else
+ emitSucceeded();
}
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h
index ed92394c..413eb2d1 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h
@@ -2,29 +2,31 @@
#include <QDebug>
#include <QObject>
-#include <QRunnable>
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModDetails.h"
-class LocalModParseTask : public QObject, public QRunnable
+#include "tasks/Task.h"
+
+class LocalModParseTask : public Task
{
Q_OBJECT
public:
struct Result {
- QString id;
- std::shared_ptr<ModDetails> details;
+ ModDetails details;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
return m_result;
}
- LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
- void run();
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
+ void executeTask() override;
-signals:
- void finished(int token);
+ [[nodiscard]] int token() const { return m_token; }
private:
void processAsZip();
@@ -33,7 +35,9 @@ private:
private:
int m_token;
- Mod::ModType m_type;
+ ResourceType m_type;
QFileInfo m_modFile;
ResultPtr m_result;
+
+ std::atomic<bool> m_aborted = false;
};
diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
new file mode 100644
index 00000000..4f87bc13
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LocalResourcePackParseTask.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include <quazip/quazip.h>
+#include <quazip/quazipfile.h>
+
+#include <QCryptographicHash>
+
+namespace ResourcePackUtils {
+
+bool process(ResourcePack& pack)
+{
+ switch (pack.type()) {
+ case ResourceType::FOLDER:
+ ResourcePackUtils::processFolder(pack);
+ return true;
+ case ResourceType::ZIPFILE:
+ ResourcePackUtils::processZIP(pack);
+ return true;
+ default:
+ qWarning() << "Invalid type for resource pack parse task!";
+ return false;
+ }
+}
+
+void processFolder(ResourcePack& pack)
+{
+ Q_ASSERT(pack.type() == ResourceType::FOLDER);
+
+ QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
+ if (mcmeta_file_info.isFile()) {
+ QFile mcmeta_file(mcmeta_file_info.filePath());
+ if (!mcmeta_file.open(QIODevice::ReadOnly))
+ return;
+
+ auto data = mcmeta_file.readAll();
+
+ ResourcePackUtils::processMCMeta(pack, std::move(data));
+
+ mcmeta_file.close();
+ }
+
+ QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
+ if (image_file_info.isFile()) {
+ QFile mcmeta_file(image_file_info.filePath());
+ if (!mcmeta_file.open(QIODevice::ReadOnly))
+ return;
+
+ auto data = mcmeta_file.readAll();
+
+ ResourcePackUtils::processPackPNG(pack, std::move(data));
+
+ mcmeta_file.close();
+ }
+}
+
+void processZIP(ResourcePack& pack)
+{
+ Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
+
+ QuaZip zip(pack.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ if (zip.setCurrentFile("pack.mcmeta")) {
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open file in zip.";
+ zip.close();
+ return;
+ }
+
+ auto data = file.readAll();
+
+ ResourcePackUtils::processMCMeta(pack, std::move(data));
+
+ file.close();
+ }
+
+ if (zip.setCurrentFile("pack.png")) {
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open file in zip.";
+ zip.close();
+ return;
+ }
+
+ auto data = file.readAll();
+
+ ResourcePackUtils::processPackPNG(pack, std::move(data));
+
+ file.close();
+ }
+
+ zip.close();
+}
+
+// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
+void processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
+{
+ try {
+ auto json_doc = QJsonDocument::fromJson(raw_data);
+ auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
+
+ pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
+ pack.setDescription(Json::ensureString(pack_obj, "description", ""));
+ } catch (Json::JsonException& e) {
+ qWarning() << "JsonException: " << e.what() << e.cause();
+ }
+}
+
+void processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
+{
+ auto img = QImage::fromData(raw_data);
+ if (!img.isNull()) {
+ pack.setImage(img);
+ } else {
+ qWarning() << "Failed to parse pack.png.";
+ }
+}
+} // namespace ResourcePackUtils
+
+LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp)
+ : Task(nullptr, false), m_token(token), m_resource_pack(rp)
+{}
+
+bool LocalResourcePackParseTask::abort()
+{
+ m_aborted = true;
+ return true;
+}
+
+void LocalResourcePackParseTask::executeTask()
+{
+ Q_ASSERT(m_resource_pack.valid());
+
+ if (!ResourcePackUtils::process(m_resource_pack))
+ return;
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
new file mode 100644
index 00000000..d3c25464
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDebug>
+#include <QObject>
+
+#include "minecraft/mod/ResourcePack.h"
+
+#include "tasks/Task.h"
+
+namespace ResourcePackUtils {
+bool process(ResourcePack& pack);
+
+void processZIP(ResourcePack& pack);
+void processFolder(ResourcePack& pack);
+
+void processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
+void processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
+} // namespace ResourcePackUtils
+
+class LocalResourcePackParseTask : public Task {
+ Q_OBJECT
+ public:
+ LocalResourcePackParseTask(int token, ResourcePack& rp);
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ void executeTask() override;
+
+ [[nodiscard]] int token() const { return m_token; }
+
+ private:
+ int m_token;
+
+ ResourcePack& m_resource_pack;
+
+ bool m_aborted = false;
+};
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
index a2e055ba..3a857740 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
@@ -38,11 +38,11 @@
#include "minecraft/mod/MetadataHandler.h"
-ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed)
- : m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_result(new Result())
+ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan)
+ : Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
{}
-void ModFolderLoadTask::run()
+void ModFolderLoadTask::executeTask()
{
if (m_is_indexed) {
// Read metadata first
@@ -52,7 +52,7 @@ void ModFolderLoadTask::run()
// Read JAR files that don't have metadata
m_mods_dir.refresh();
for (auto entry : m_mods_dir.entryInfoList()) {
- Mod::Ptr mod(new Mod(entry));
+ Mod* mod(new Mod(entry));
if (mod->enabled()) {
if (m_result->mods.contains(mod->internal_id())) {
@@ -83,7 +83,23 @@ void ModFolderLoadTask::run()
}
}
- emit succeeded();
+ // Remove orphan metadata to prevent issues
+ // See https://github.com/PolyMC/PolyMC/issues/996
+ if (m_clean_orphan) {
+ 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();
+ }
+ }
+ }
+
+ if (m_aborted)
+ emit finished();
+ else
+ emitSucceeded();
}
void ModFolderLoadTask::getFromMetadata()
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
index 0b6bb6cc..0f18b8b9 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
@@ -42,8 +42,9 @@
#include <QRunnable>
#include <memory>
#include "minecraft/mod/Mod.h"
+#include "tasks/Task.h"
-class ModFolderLoadTask : public QObject, public QRunnable
+class ModFolderLoadTask : public Task
{
Q_OBJECT
public:
@@ -56,16 +57,26 @@ public:
}
public:
- ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed);
- void run();
-signals:
- void succeeded();
+ ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false);
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override
+ {
+ m_aborted.store(true);
+ return true;
+ }
+
+
+ void executeTask() override;
private:
void getFromMetadata();
private:
- QDir& m_mods_dir, m_index_dir;
+ QDir m_mods_dir, m_index_dir;
bool m_is_indexed;
+ bool m_clean_orphan;
ResultPtr m_result;
+
+ std::atomic<bool> m_aborted = false;
};
diff --git a/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt b/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt
deleted file mode 100644
index 8d1c8b69..00000000
--- a/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta b/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta
deleted file mode 100644
index 67ee0434..00000000
--- a/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "pack": {
- "pack_format": 1,
- "description": "Some resource pack maybe"
- }
-}
diff --git a/launcher/minecraft/mod/testdata/test_folder/pack.nfo b/launcher/minecraft/mod/testdata/test_folder/pack.nfo
deleted file mode 100644
index 8d1c8b69..00000000
--- a/launcher/minecraft/mod/testdata/test_folder/pack.nfo
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/launcher/minecraft/testdata/1.9-simple.json b/launcher/minecraft/testdata/1.9-simple.json
deleted file mode 100644
index 574c5b06..00000000
--- a/launcher/minecraft/testdata/1.9-simple.json
+++ /dev/null
@@ -1,198 +0,0 @@
-{
- "assets": "1.9",
- "id": "1.9",
- "libraries": [
- {
- "name": "oshi-project:oshi-core:1.1"
- },
- {
- "name": "net.java.dev.jna:jna:3.4.0"
- },
- {
- "name": "net.java.dev.jna:platform:3.4.0"
- },
- {
- "name": "com.ibm.icu:icu4j-core-mojang:51.2"
- },
- {
- "name": "net.sf.jopt-simple:jopt-simple:4.6"
- },
- {
- "name": "com.paulscode:codecjorbis:20101023"
- },
- {
- "name": "com.paulscode:codecwav:20101023"
- },
- {
- "name": "com.paulscode:libraryjavasound:20101123"
- },
- {
- "name": "com.paulscode:librarylwjglopenal:20100824"
- },
- {
- "name": "com.paulscode:soundsystem:20120107"
- },
- {
- "name": "io.netty:netty-all:4.0.23.Final"
- },
- {
- "name": "com.google.guava:guava:17.0"
- },
- {
- "name": "org.apache.commons:commons-lang3:3.3.2"
- },
- {
- "name": "commons-io:commons-io:2.4"
- },
- {
- "name": "commons-codec:commons-codec:1.9"
- },
- {
- "name": "net.java.jinput:jinput:2.0.5"
- },
- {
- "name": "net.java.jutils:jutils:1.0.0"
- },
- {
- "name": "com.google.code.gson:gson:2.2.4"
- },
- {
- "name": "com.mojang:authlib:1.5.22"
- },
- {
- "name": "com.mojang:realms:1.8.4"
- },
- {
- "name": "org.apache.commons:commons-compress:1.8.1"
- },
- {
- "name": "org.apache.httpcomponents:httpclient:4.3.3"
- },
- {
- "name": "commons-logging:commons-logging:1.1.3"
- },
- {
- "name": "org.apache.httpcomponents:httpcore:4.3.2"
- },
- {
- "name": "org.apache.logging.log4j:log4j-api:2.0-beta9"
- },
- {
- "name": "org.apache.logging.log4j:log4j-core:2.0-beta9"
- },
- {
- "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209",
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209",
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- },
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822",
- "rules": [
- {
- "action": "allow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822",
- "rules": [
- {
- "action": "allow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- },
- "rules": [
- {
- "action": "allow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "net.java.jinput:jinput-platform:2.0.5",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- }
- }
- ],
- "mainClass": "net.minecraft.client.main.Main",
- "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}",
- "minimumLauncherVersion": 18,
- "releaseTime": "2016-02-29T13:49:54+00:00",
- "time": "2016-03-01T13:14:53+00:00",
- "type": "release"
-}
diff --git a/launcher/minecraft/testdata/1.9.json b/launcher/minecraft/testdata/1.9.json
deleted file mode 100644
index 697c6059..00000000
--- a/launcher/minecraft/testdata/1.9.json
+++ /dev/null
@@ -1,529 +0,0 @@
-{
- "assetIndex": {
- "id": "1.9",
- "sha1": "cde65b47a43f638653ab1da3848b53f8a7477b16",
- "size": 136916,
- "totalSize": 119917473,
- "url": "https://launchermeta.mojang.com/mc-staging/assets/1.9/cde65b47a43f638653ab1da3848b53f8a7477b16/1.9.json"
- },
- "assets": "1.9",
- "downloads": {
- "client": {
- "sha1": "2f67dfe8953299440d1902f9124f0f2c3a2c940f",
- "size": 8697592,
- "url": "https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar"
- },
- "server": {
- "sha1": "b4d449cf2918e0f3bd8aa18954b916a4d1880f0d",
- "size": 8848015,
- "url": "https://launcher.mojang.com/mc/game/1.9/server/b4d449cf2918e0f3bd8aa18954b916a4d1880f0d/server.jar"
- }
- },
- "id": "1.9",
- "libraries": [
- {
- "downloads": {
- "artifact": {
- "path": "oshi-project/oshi-core/1.1/oshi-core-1.1.jar",
- "sha1": "9ddf7b048a8d701be231c0f4f95fd986198fd2d8",
- "size": 30973,
- "url": "https://libraries.minecraft.net/oshi-project/oshi-core/1.1/oshi-core-1.1.jar"
- }
- },
- "name": "oshi-project:oshi-core:1.1"
- },
- {
- "downloads": {
- "artifact": {
- "path": "net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar",
- "sha1": "803ff252fedbd395baffd43b37341dc4a150a554",
- "size": 1008730,
- "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar"
- }
- },
- "name": "net.java.dev.jna:jna:3.4.0"
- },
- {
- "downloads": {
- "artifact": {
- "path": "net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar",
- "sha1": "e3f70017be8100d3d6923f50b3d2ee17714e9c13",
- "size": 913436,
- "url": "https://libraries.minecraft.net/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar"
- }
- },
- "name": "net.java.dev.jna:platform:3.4.0"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar",
- "sha1": "63d216a9311cca6be337c1e458e587f99d382b84",
- "size": 1634692,
- "url": "https://libraries.minecraft.net/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar"
- }
- },
- "name": "com.ibm.icu:icu4j-core-mojang:51.2"
- },
- {
- "downloads": {
- "artifact": {
- "path": "net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar",
- "sha1": "306816fb57cf94f108a43c95731b08934dcae15c",
- "size": 62477,
- "url": "https://libraries.minecraft.net/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar"
- }
- },
- "name": "net.sf.jopt-simple:jopt-simple:4.6"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar",
- "sha1": "c73b5636faf089d9f00e8732a829577de25237ee",
- "size": 103871,
- "url": "https://libraries.minecraft.net/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar"
- }
- },
- "name": "com.paulscode:codecjorbis:20101023"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar",
- "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da",
- "size": 5618,
- "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"
- }
- },
- "name": "com.paulscode:codecwav:20101023"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar",
- "sha1": "5c5e304366f75f9eaa2e8cca546a1fb6109348b3",
- "size": 21679,
- "url": "https://libraries.minecraft.net/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar"
- }
- },
- "name": "com.paulscode:libraryjavasound:20101123"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar",
- "sha1": "73e80d0794c39665aec3f62eee88ca91676674ef",
- "size": 18981,
- "url": "https://libraries.minecraft.net/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar"
- }
- },
- "name": "com.paulscode:librarylwjglopenal:20100824"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/paulscode/soundsystem/20120107/soundsystem-20120107.jar",
- "sha1": "419c05fe9be71f792b2d76cfc9b67f1ed0fec7f6",
- "size": 65020,
- "url": "https://libraries.minecraft.net/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar"
- }
- },
- "name": "com.paulscode:soundsystem:20120107"
- },
- {
- "downloads": {
- "artifact": {
- "path": "io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar",
- "sha1": "0294104aaf1781d6a56a07d561e792c5d0c95f45",
- "size": 1779991,
- "url": "https://libraries.minecraft.net/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar"
- }
- },
- "name": "io.netty:netty-all:4.0.23.Final"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/google/guava/guava/17.0/guava-17.0.jar",
- "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445",
- "size": 2243036,
- "url": "https://libraries.minecraft.net/com/google/guava/guava/17.0/guava-17.0.jar"
- }
- },
- "name": "com.google.guava:guava:17.0"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar",
- "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3",
- "size": 412739,
- "url": "https://libraries.minecraft.net/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar"
- }
- },
- "name": "org.apache.commons:commons-lang3:3.3.2"
- },
- {
- "downloads": {
- "artifact": {
- "path": "commons-io/commons-io/2.4/commons-io-2.4.jar",
- "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad",
- "size": 185140,
- "url": "https://libraries.minecraft.net/commons-io/commons-io/2.4/commons-io-2.4.jar"
- }
- },
- "name": "commons-io:commons-io:2.4"
- },
- {
- "downloads": {
- "artifact": {
- "path": "commons-codec/commons-codec/1.9/commons-codec-1.9.jar",
- "sha1": "9ce04e34240f674bc72680f8b843b1457383161a",
- "size": 263965,
- "url": "https://libraries.minecraft.net/commons-codec/commons-codec/1.9/commons-codec-1.9.jar"
- }
- },
- "name": "commons-codec:commons-codec:1.9"
- },
- {
- "downloads": {
- "artifact": {
- "path": "net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar",
- "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4",
- "size": 208338,
- "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar"
- }
- },
- "name": "net.java.jinput:jinput:2.0.5"
- },
- {
- "downloads": {
- "artifact": {
- "path": "net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar",
- "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6",
- "size": 7508,
- "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar"
- }
- },
- "name": "net.java.jutils:jutils:1.0.0"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/google/code/gson/gson/2.2.4/gson-2.2.4.jar",
- "sha1": "a60a5e993c98c864010053cb901b7eab25306568",
- "size": 190432,
- "url": "https://libraries.minecraft.net/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar"
- }
- },
- "name": "com.google.code.gson:gson:2.2.4"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/mojang/authlib/1.5.22/authlib-1.5.22.jar",
- "sha1": "afaa8f6df976fcb5520e76ef1d5798c9e6b5c0b2",
- "size": 64539,
- "url": "https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar"
- }
- },
- "name": "com.mojang:authlib:1.5.22"
- },
- {
- "downloads": {
- "artifact": {
- "path": "com/mojang/realms/1.8.4/realms-1.8.4.jar",
- "sha1": "15f8dc326c97a96dee6e65392e145ad6d1cb46cb",
- "size": 1131574,
- "url": "https://libraries.minecraft.net/com/mojang/realms/1.8.4/realms-1.8.4.jar"
- }
- },
- "name": "com.mojang:realms:1.8.4"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar",
- "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d",
- "size": 365552,
- "url": "https://libraries.minecraft.net/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar"
- }
- },
- "name": "org.apache.commons:commons-compress:1.8.1"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar",
- "sha1": "18f4247ff4572a074444572cee34647c43e7c9c7",
- "size": 589512,
- "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar"
- }
- },
- "name": "org.apache.httpcomponents:httpclient:4.3.3"
- },
- {
- "downloads": {
- "artifact": {
- "path": "commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar",
- "sha1": "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f",
- "size": 62050,
- "url": "https://libraries.minecraft.net/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar"
- }
- },
- "name": "commons-logging:commons-logging:1.1.3"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar",
- "sha1": "31fbbff1ddbf98f3aa7377c94d33b0447c646b6e",
- "size": 282269,
- "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar"
- }
- },
- "name": "org.apache.httpcomponents:httpcore:4.3.2"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar",
- "sha1": "1dd66e68cccd907880229f9e2de1314bd13ff785",
- "size": 108161,
- "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar"
- }
- },
- "name": "org.apache.logging.log4j:log4j-api:2.0-beta9"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar",
- "sha1": "678861ba1b2e1fccb594bb0ca03114bb05da9695",
- "size": 681134,
- "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar"
- }
- },
- "name": "org.apache.logging.log4j:log4j-core:2.0-beta9"
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar",
- "sha1": "697517568c68e78ae0b4544145af031c81082dfe",
- "size": 1047168,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar"
- }
- },
- "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209",
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar",
- "sha1": "d51a7c040a721d13efdfbd34f8b257b2df882ad0",
- "size": 173887,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar"
- }
- },
- "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209",
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar",
- "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
- "size": 22,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar"
- },
- "classifiers": {
- "natives-linux": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar",
- "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b",
- "size": 578680,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar"
- },
- "natives-osx": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar",
- "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed",
- "size": 426822,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"
- },
- "natives-windows": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar",
- "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0",
- "size": 613748,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar"
- }
- }
- },
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- },
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar",
- "sha1": "7707204c9ffa5d91662de95f0a224e2f721b22af",
- "size": 1045632,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar"
- }
- },
- "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822",
- "rules": [
- {
- "action": "allow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "downloads": {
- "artifact": {
- "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar",
- "sha1": "f0e612c840a7639c1f77f68d72a28dae2f0c8490",
- "size": 173887,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar"
- }
- },
- "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822",
- "rules": [
- {
- "action": "allow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "downloads": {
- "classifiers": {
- "natives-linux": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar",
- "sha1": "d898a33b5d0a6ef3fed3a4ead506566dce6720a5",
- "size": 578539,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar"
- },
- "natives-osx": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar",
- "sha1": "79f5ce2fea02e77fe47a3c745219167a542121d7",
- "size": 468116,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar"
- },
- "natives-windows": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar",
- "sha1": "78b2a55ce4dc29c6b3ec4df8ca165eba05f9b341",
- "size": 613680,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar"
- }
- }
- },
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- },
- "rules": [
- {
- "action": "allow",
- "os": {
- "name": "osx"
- }
- }
- ]
- },
- {
- "downloads": {
- "classifiers": {
- "natives-linux": {
- "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar",
- "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795",
- "size": 10362,
- "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar"
- },
- "natives-osx": {
- "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar",
- "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232",
- "size": 12186,
- "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar"
- },
- "natives-windows": {
- "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar",
- "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16",
- "size": 155179,
- "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar"
- }
- }
- },
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "net.java.jinput:jinput-platform:2.0.5",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- }
- }
- ],
- "mainClass": "net.minecraft.client.main.Main",
- "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}",
- "minimumLauncherVersion": 18,
- "releaseTime": "2016-02-29T13:49:54+00:00",
- "time": "2016-03-01T13:14:53+00:00",
- "type": "release"
-}
diff --git a/launcher/minecraft/testdata/codecwav-20101023.jar b/launcher/minecraft/testdata/codecwav-20101023.jar
deleted file mode 100644
index f5236083..00000000
--- a/launcher/minecraft/testdata/codecwav-20101023.jar
+++ /dev/null
@@ -1 +0,0 @@
-dummy test file.
diff --git a/launcher/minecraft/testdata/lib-native-arch.json b/launcher/minecraft/testdata/lib-native-arch.json
deleted file mode 100644
index 501826ae..00000000
--- a/launcher/minecraft/testdata/lib-native-arch.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "downloads": {
- "classifiers": {
- "natives-osx": {
- "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar",
- "sha1": "62503ee712766cf77f97252e5902786fd834b8c5",
- "size": 418331,
- "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar"
- },
- "natives-windows-32": {
- "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar",
- "sha1": "7c6affe439099806a4f552da14c42f9d643d8b23",
- "size": 386792,
- "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"
- },
- "natives-windows-64": {
- "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar",
- "sha1": "39d0c3d363735b4785598e0e7fbf8297c706a9f9",
- "size": 463390,
- "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"
- }
- }
- },
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "tv.twitch:twitch-platform:5.16",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows-${arch}"
- },
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "linux"
- }
- }
- ]
-}
diff --git a/launcher/minecraft/testdata/lib-native.json b/launcher/minecraft/testdata/lib-native.json
deleted file mode 100644
index 5b9f3b55..00000000
--- a/launcher/minecraft/testdata/lib-native.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "downloads": {
- "artifact": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar",
- "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
- "size": 22,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar"
- },
- "classifiers": {
- "natives-linux": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar",
- "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b",
- "size": 578680,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar"
- },
- "natives-osx": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar",
- "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed",
- "size": 426822,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"
- },
- "natives-windows": {
- "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar",
- "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0",
- "size": 613748,
- "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar"
- }
- }
- },
- "extract": {
- "exclude": [
- "META-INF/"
- ]
- },
- "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
- "natives": {
- "linux": "natives-linux",
- "osx": "natives-osx",
- "windows": "natives-windows"
- },
- "rules": [
- {
- "action": "allow"
- },
- {
- "action": "disallow",
- "os": {
- "name": "osx"
- }
- }
- ]
-}
diff --git a/launcher/minecraft/testdata/lib-simple.json b/launcher/minecraft/testdata/lib-simple.json
deleted file mode 100644
index 90bbff07..00000000
--- a/launcher/minecraft/testdata/lib-simple.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "downloads": {
- "artifact": {
- "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar",
- "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da",
- "size": 5618,
- "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"
- }
- },
- "name": "com.paulscode:codecwav:20101023"
-}
diff --git a/launcher/minecraft/testdata/testname-testversion-linux-32.jar b/launcher/minecraft/testdata/testname-testversion-linux-32.jar
deleted file mode 100644
index f5236083..00000000
--- a/launcher/minecraft/testdata/testname-testversion-linux-32.jar
+++ /dev/null
@@ -1 +0,0 @@
-dummy test file.
diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp
index b6238ce9..7a0bd2f3 100644
--- a/launcher/minecraft/update/FMLLibrariesTask.cpp
+++ b/launcher/minecraft/update/FMLLibrariesTask.cpp
@@ -63,11 +63,12 @@ void FMLLibrariesTask::executeTask()
setStatus(tr("Downloading FML libraries..."));
auto dljob = new NetJob("FML libraries", APPLICATION->network());
auto metacache = APPLICATION->metacache();
+ Net::Download::Options options = Net::Download::Option::MakeEternal;
for (auto &lib : fmlLibsToProcess)
{
auto entry = metacache->resolveEntry("fmllibs", lib.filename);
QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename;
- dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry));
+ dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options));
}
connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 60c54c4e..234330a7 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -3,81 +3,73 @@
#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)
+EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov)
+ : Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
{
- auto hash = getHash(mod);
- if (hash.isEmpty())
- emitFail(mod);
- else
- m_mods.insert(hash, mod);
+ auto hash_task = createNewHash(mod);
+ if (!hash_task)
+ return;
+ connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
+ connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
+ hash_task->start();
}
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
- : Task(nullptr), m_index_dir(dir), m_provider(prov)
+ : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{
+ m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
for (auto* mod : mods) {
- if (!mod->valid()) {
- emitFail(mod);
- continue;
- }
-
- auto hash = getHash(mod);
- if (hash.isEmpty()) {
- emitFail(mod);
+ auto hash_task = createNewHash(mod);
+ if (!hash_task)
continue;
- }
-
- m_mods.insert(hash, mod);
+ connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
+ connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
+ m_hashing_task->addTask(hash_task);
}
}
-QString EnsureMetadataTask::getHash(Mod* mod)
+Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(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();
+ if (!mod || !mod->valid() || mod->type() == ResourceType::FOLDER)
+ return nullptr;
- return {};
- }
-
- switch (m_provider) {
- case ModPlatform::Provider::MODRINTH: {
- auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider);
+}
- 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);
- }
+QString EnsureMetadataTask::getExistingHash(Mod* mod)
+{
+ // Check for already computed hashes
+ // (linear on the number of mods vs. linear on the size of the mod's JAR)
+ auto it = m_mods.keyValueBegin();
+ while (it != m_mods.keyValueEnd()) {
+ if ((*it).second == mod)
+ break;
+ it++;
+ }
- return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
- }
+ // We already have the hash computed
+ if (it != m_mods.keyValueEnd()) {
+ return (*it).first;
}
+ // No existing hash
return {};
}
@@ -110,7 +102,7 @@ void EnsureMetadataTask::executeTask()
}
// Folders don't have metadata
- if (mod->type() == Mod::MOD_FOLDER) {
+ if (mod->type() == ResourceType::FOLDER) {
emitReady(mod);
}
}
@@ -127,11 +119,9 @@ void EnsureMetadataTask::executeTask()
}
auto invalidade_leftover = [this] {
- QMutableHashIterator<QString, Mod*> mods_iter(m_mods);
- while (mods_iter.hasNext()) {
- auto mod = mods_iter.next();
- emitFail(mod.value());
- }
+ for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++)
+ emitFail(mod.value(), mod.key(), RemoveFromList::No);
+ m_mods.clear();
emitSucceeded();
};
@@ -178,20 +168,44 @@ void EnsureMetadataTask::executeTask()
version_task->start();
}
-void EnsureMetadataTask::emitReady(Mod* m)
+void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove)
{
+ if (!m) {
+ qCritical() << "Tried to mark a null mod as ready.";
+ if (!key.isEmpty())
+ m_mods.remove(key);
+
+ return;
+ }
+
qDebug() << QString("Generated metadata for %1").arg(m->name());
emit metadataReady(m);
- m_mods.remove(getHash(m));
+ if (remove == RemoveFromList::Yes) {
+ if (key.isEmpty())
+ key = getExistingHash(m);
+ m_mods.remove(key);
+ }
}
-void EnsureMetadataTask::emitFail(Mod* m)
+void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
{
+ if (!m) {
+ qCritical() << "Tried to mark a null mod as failed.";
+ if (!key.isEmpty())
+ m_mods.remove(key);
+
+ return;
+ }
+
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
emit metadataFailed(m);
- m_mods.remove(getHash(m));
+ if (remove == RemoveFromList::Yes) {
+ if (key.isEmpty())
+ key = getExistingHash(m);
+ m_mods.remove(key);
+ }
}
// Modrinth
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
index 79db6976..a8b0851e 100644
--- a/launcher/modplatform/EnsureMetadataTask.h
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -1,12 +1,14 @@
#pragma once
#include "ModIndex.h"
-#include "tasks/SequentialTask.h"
#include "net/NetJob.h"
+#include "modplatform/helpers/HashUtils.h"
+
+#include "tasks/ConcurrentTask.h"
+
class Mod;
class QDir;
-class MultipleOptionsTask;
class EnsureMetadataTask : public Task {
Q_OBJECT
@@ -17,6 +19,8 @@ class EnsureMetadataTask : public Task {
~EnsureMetadataTask() = default;
+ Task::Ptr getHashingTask() { return m_hashing_task; }
+
public slots:
bool abort() override;
protected slots:
@@ -31,10 +35,16 @@ class EnsureMetadataTask : public Task {
auto flameProjectsTask() -> NetJob::Ptr;
// Helpers
- void emitReady(Mod*);
- void emitFail(Mod*);
+ enum class RemoveFromList {
+ Yes,
+ No
+ };
+ void emitReady(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
+ void emitFail(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
- auto getHash(Mod*) -> QString;
+ // Hashes and stuff
+ auto createNewHash(Mod*) -> Hashing::Hasher::Ptr;
+ auto getExistingHash(Mod*) -> QString;
private slots:
void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
@@ -50,5 +60,6 @@ class EnsureMetadataTask : public Task {
ModPlatform::Provider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
+ ConcurrentTask* m_hashing_task;
NetJob* m_current_task;
};
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
index 4114d83c..c7408835 100644
--- a/launcher/modplatform/ModAPI.h
+++ b/launcher/modplatform/ModAPI.h
@@ -73,7 +73,7 @@ class ModAPI {
};
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
- virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0;
+ virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) = 0;
virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
@@ -85,7 +85,7 @@ class ModAPI {
ModLoaderTypes loaders;
};
- virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0;
+ virtual void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const = 0;
static auto getModLoaderString(ModLoaderType type) -> const QString {
switch (type) {
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 3c4b7887..34fd9f30 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -19,6 +19,8 @@
#include "modplatform/ModIndex.h"
#include <QCryptographicHash>
+#include <QDebug>
+#include <QIODevice>
namespace ModPlatform {
@@ -53,34 +55,26 @@ auto ProviderCapabilities::hashType(Provider p) -> QStringList
}
return {};
}
-auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray
+
+auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString
{
+ QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) {
case Provider::MODRINTH: {
- // NOTE: Data is the result of reading the entire JAR file!
-
- // If 'type' was specified, we use that
- if (!type.isEmpty() && hashType(p).contains(type)) {
- if (type == "sha512")
- return QCryptographicHash::hash(data, QCryptographicHash::Sha512);
- else if (type == "sha1")
- return QCryptographicHash::hash(data, QCryptographicHash::Sha1);
- }
-
- return QCryptographicHash::hash(data, QCryptographicHash::Sha512);
+ algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
+ break;
}
case Provider::FLAME:
- // If 'type' was specified, we use that
- if (!type.isEmpty() && hashType(p).contains(type)) {
- if(type == "sha1")
- return QCryptographicHash::hash(data, QCryptographicHash::Sha1);
- else if (type == "md5")
- return QCryptographicHash::hash(data, QCryptographicHash::Md5);
- }
-
+ algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break;
}
- return {};
+
+ QCryptographicHash hash(algo);
+ if(!hash.addData(device))
+ qCritical() << "Failed to read JAR to create hash!";
+
+ Q_ASSERT(hash.result().length() == hash.hashLength(algo));
+ return { hash.result().toHex() };
}
} // namespace ModPlatform
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index dc297d03..518fed7c 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -24,6 +24,8 @@
#include <QVariant>
#include <QVector>
+class QIODevice;
+
namespace ModPlatform {
enum class Provider {
@@ -36,7 +38,7 @@ class ProviderCapabilities {
auto name(Provider) -> const char*;
auto readableName(Provider) -> QString;
auto hashType(Provider) -> QStringList;
- auto hash(Provider, QByteArray&, QString type = "") -> QByteArray;
+ auto hash(Provider, QIODevice*, QString type = "") -> QString;
};
struct ModpackAuthor {
@@ -73,6 +75,8 @@ struct ExtraPackData {
QString sourceUrl;
QString wikiUrl;
QString discordUrl;
+
+ QString body;
};
struct IndexedPack {
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 0ed0ad29..70a35395 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -60,12 +60,13 @@ namespace ATLauncher {
static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version);
-PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version)
+PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode)
{
m_support = support;
m_pack_name = packName;
m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), "");
m_version_name = version;
+ m_install_mode = installMode;
}
bool PackInstallTask::abort()
@@ -117,9 +118,30 @@ void PackInstallTask::onDownloadSucceeded()
}
m_version = version;
- // Display install message if one exists
- if (!m_version.messages.install.isEmpty())
- m_support->displayMessage(m_version.messages.install);
+ // Derived from the installation mode
+ QString message;
+ bool resetDirectory;
+
+ switch (m_install_mode) {
+ case InstallMode::Reinstall:
+ case InstallMode::Update:
+ message = m_version.messages.update;
+ resetDirectory = true;
+ break;
+
+ case InstallMode::Install:
+ message = m_version.messages.install;
+ resetDirectory = false;
+ break;
+
+ default:
+ emitFailed(tr("Unsupported installation mode"));
+ return;
+ }
+
+ // Display message if one exists
+ if (!message.isEmpty())
+ m_support->displayMessage(message);
auto ver = getComponentVersion("net.minecraft", m_version.minecraft);
if (!ver) {
@@ -128,6 +150,10 @@ void PackInstallTask::onDownloadSucceeded()
}
minecraftVersion = ver;
+ if (resetDirectory) {
+ deleteExistingFiles();
+ }
+
if(m_version.noConfigs) {
downloadMods();
}
@@ -143,6 +169,116 @@ void PackInstallTask::onDownloadFailed(QString reason)
emitFailed(reason);
}
+void PackInstallTask::deleteExistingFiles()
+{
+ setStatus(tr("Deleting existing files..."));
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete
+ VersionDeletes deletes;
+ deletes.folders.append(VersionDelete{ "root", "mods%s%" });
+ deletes.folders.append(VersionDelete{ "root", "configs%s%" });
+ deletes.folders.append(VersionDelete{ "root", "bin%s%" });
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep
+ VersionKeeps keeps;
+ keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" });
+ keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" });
+ keeps.files.append(VersionKeep{ "root", "options.txt" });
+ keeps.files.append(VersionKeep{ "root", "servers.dat" });
+
+ // Merge with version deletes and keeps
+ for (const auto& item : m_version.deletes.files)
+ deletes.files.append(item);
+ for (const auto& item : m_version.deletes.folders)
+ deletes.folders.append(item);
+ for (const auto& item : m_version.keeps.files)
+ keeps.files.append(item);
+ for (const auto& item : m_version.keeps.folders)
+ keeps.folders.append(item);
+
+ auto getPathForBase = [this](const QString& base) {
+ auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft");
+
+ if (base == "root") {
+ return minecraftPath;
+ }
+ else if (base == "config") {
+ return FS::PathCombine(minecraftPath, "config");
+ }
+ else {
+ qWarning() << "Unrecognised base path" << base;
+ return minecraftPath;
+ }
+ };
+
+ auto convertToSystemPath = [](const QString& path) {
+ auto t = path;
+ t.replace("%s%", QDir::separator());
+ return t;
+ };
+
+ auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) {
+ for (const auto& item : keeps.files) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath == path) {
+ return true;
+ }
+ }
+
+ for (const auto& item : keeps.folders) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath.startsWith(path)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ // Keep track of files to delete
+ QSet<QString> filesToDelete;
+
+ for (const auto& item : deletes.files) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ if (shouldKeep(fullPath))
+ continue;
+
+ filesToDelete.insert(fullPath);
+ }
+
+ for (const auto& item : deletes.folders) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ QDirIterator it(fullPath, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto path = it.next();
+
+ if (shouldKeep(path))
+ continue;
+
+ filesToDelete.insert(path);
+ }
+ }
+
+ // Delete the files
+ for (const auto& item : filesToDelete) {
+ QFile::remove(item);
+ }
+}
+
QString PackInstallTask::getDirForModType(ModType type, QString raw)
{
switch (type) {
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
index f55873e9..a7124d59 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -46,10 +46,16 @@
#include "minecraft/PackProfile.h"
#include "meta/Version.h"
-#include <nonstd/optional>
+#include <optional>
namespace ATLauncher {
+enum class InstallMode {
+ Install,
+ Reinstall,
+ Update,
+};
+
class UserInteractionSupport {
public:
@@ -75,7 +81,7 @@ class PackInstallTask : public InstanceTask
Q_OBJECT
public:
- explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version);
+ explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install);
virtual ~PackInstallTask(){}
bool canAbort() const override { return true; }
@@ -99,6 +105,7 @@ private:
bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+ void deleteExistingFiles();
void installConfigs();
void extractConfigs();
void downloadMods();
@@ -117,6 +124,7 @@ private:
NetJob::Ptr jobPtr;
QByteArray response;
+ InstallMode m_install_mode;
QString m_pack_name;
QString m_pack_safe_name;
QString m_version_name;
@@ -131,8 +139,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/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
index 3af02a09..5a458f4e 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -224,6 +224,64 @@ static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a,
a.depends = Json::ensureString(obj, "depends", "");
}
+static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj)
+{
+ k.base = Json::requireString(obj, "base");
+ k.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj)
+{
+ if (obj.contains("files")) {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto keepRaw : files) {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.files.append(keep);
+ }
+ }
+
+ if (obj.contains("folders")) {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto keepRaw : folders) {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.folders.append(keep);
+ }
+ }
+}
+
+static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj)
+{
+ d.base = Json::requireString(obj, "base");
+ d.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj)
+{
+ if (obj.contains("files")) {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto deleteRaw : files) {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.files.append(versionDelete);
+ }
+ }
+
+ if (obj.contains("folders")) {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto deleteRaw : folders) {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.folders.append(versionDelete);
+ }
+ }
+}
+
void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
{
v.version = Json::requireString(obj, "version");
@@ -284,4 +342,10 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
auto messages = Json::ensureObject(obj, "messages");
loadVersionMessages(v.messages, messages);
+
+ auto keeps = Json::ensureObject(obj, "keeps");
+ loadVersionKeeps(v.keeps, keeps);
+
+ auto deletes = Json::ensureObject(obj, "deletes");
+ loadVersionDeletes(v.deletes, deletes);
}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
index 43510c50..571c976d 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.h
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -150,6 +150,26 @@ struct VersionMessages
QString update;
};
+struct VersionKeep {
+ QString base;
+ QString target;
+};
+
+struct VersionKeeps {
+ QVector<VersionKeep> files;
+ QVector<VersionKeep> folders;
+};
+
+struct VersionDelete {
+ QString base;
+ QString target;
+};
+
+struct VersionDeletes {
+ QVector<VersionDelete> files;
+ QVector<VersionDelete> folders;
+};
+
struct PackVersionMainClass
{
QString mainClass;
@@ -178,6 +198,9 @@ struct PackVersion
QMap<QString, QString> colours;
QMap<QString, QString> warnings;
VersionMessages messages;
+
+ VersionKeeps keeps;
+ VersionDeletes deletes;
};
void loadVersion(PackVersion & v, QJsonObject & obj);
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
index 0ff04f72..9c74918b 100644
--- a/launcher/modplatform/flame/FlameAPI.cpp
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -67,6 +67,43 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
return changelog;
}
+auto FlameAPI::getModDescription(int modId) -> QString
+{
+ QEventLoop lock;
+ QString description;
+
+ auto* netJob = new NetJob(QString("Flame::ModDescription"), APPLICATION->network());
+ auto* response = new QByteArray();
+ netJob->addNetAction(Net::Download::makeByteArray(
+ QString("https://api.curseforge.com/v1/mods/%1/description")
+ .arg(QString::number(modId)), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &description] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Flame::ModDescription at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ netJob->failed(parse_error.errorString());
+ return;
+ }
+
+ description = Json::ensureString(doc.object(), "data");
+ });
+
+ QObject::connect(netJob, &NetJob::finished, [response, &lock] {
+ delete response;
+ lock.quit();
+ });
+
+ netJob->start();
+ lock.exec();
+
+ return description;
+}
+
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
{
QEventLoop loop;
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 336df387..4eac0664 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -7,6 +7,7 @@ class FlameAPI : public NetworkModAPI {
public:
auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
auto getModFileChangelog(int modId, int fileId) -> QString;
+ auto getModDescription(int modId) -> QString;
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 746018e2..32aa4bdb 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -4,10 +4,9 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FlameAPI.h"
-#include "net/NetJob.h"
-static ModPlatform::ProviderCapabilities ProviderCaps;
static FlameAPI api;
+static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
@@ -31,10 +30,11 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.authors.append(packAuthor);
}
- loadExtraPackData(pack, obj);
+ pack.extraDataLoaded = false;
+ loadURLs(pack, obj);
}
-void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
+void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
auto links_obj = Json::ensureObject(obj, "links");
@@ -50,7 +50,16 @@ void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
if(pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
- pack.extraDataLoaded = true;
+ if (!pack.extraData.body.isEmpty())
+ pack.extraDataLoaded = true;
+}
+
+void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj)
+{
+ pack.extraData.body = api.getModDescription(pack.addonId.toInt());
+
+ if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty())
+ pack.extraDataLoaded = true;
}
static QString enumToString(int hash_algorithm)
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index a839dd83..db63cdbb 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -12,7 +12,8 @@
namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
-void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
+void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
+void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp
new file mode 100644
index 00000000..a7bbaba5
--- /dev/null
+++ b/launcher/modplatform/helpers/HashUtils.cpp
@@ -0,0 +1,81 @@
+#include "HashUtils.h"
+
+#include <QDebug>
+#include <QFile>
+
+#include "FileSystem.h"
+
+#include <MurmurHash2.h>
+
+namespace Hashing {
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
+{
+ switch (provider) {
+ case ModPlatform::Provider::MODRINTH:
+ return createModrinthHasher(file_path);
+ case ModPlatform::Provider::FLAME:
+ return createFlameHasher(file_path);
+ default:
+ qCritical() << "[Hashing]"
+ << "Unrecognized mod platform!";
+ return nullptr;
+ }
+}
+
+Hasher::Ptr createModrinthHasher(QString file_path)
+{
+ return new ModrinthHasher(file_path);
+}
+
+Hasher::Ptr createFlameHasher(QString file_path)
+{
+ return new FlameHasher(file_path);
+}
+
+void ModrinthHasher::executeTask()
+{
+ QFile file(m_path);
+
+ try {
+ file.open(QFile::ReadOnly);
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
+ qCritical() << QString("Reason: ") << e.cause();
+
+ emitFailed("Failed to open file for hashing.");
+ return;
+ }
+
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type);
+
+ file.close();
+
+ if (m_hash.isEmpty()) {
+ emitFailed("Empty hash!");
+ } else {
+ emitSucceeded();
+ }
+}
+
+void FlameHasher::executeTask()
+{
+ // CF-specific
+ auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
+
+ std::ifstream file_stream(m_path.toStdString(), std::ifstream::binary);
+ // TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
+ // How do we make this non-blocking then?
+ m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
+
+ if (m_hash.isEmpty()) {
+ emitFailed("Empty hash!");
+ } else {
+ emitSucceeded();
+ }
+}
+
+} // namespace Hashing
diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h
new file mode 100644
index 00000000..38fddf03
--- /dev/null
+++ b/launcher/modplatform/helpers/HashUtils.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QString>
+
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+namespace Hashing {
+
+class Hasher : public Task {
+ public:
+ using Ptr = shared_qobject_ptr<Hasher>;
+
+ Hasher(QString file_path) : m_path(std::move(file_path)) {}
+
+ /* We can't really abort this task, but we can say we aborted and finish our thing quickly :) */
+ bool abort() override { return true; }
+
+ void executeTask() override = 0;
+
+ QString getResult() const { return m_hash; };
+ QString getPath() const { return m_path; };
+
+ protected:
+ QString m_hash;
+ QString m_path;
+};
+
+class FlameHasher : public Hasher {
+ public:
+ FlameHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("FlameHasher: %1").arg(file_path)); }
+
+ void executeTask() override;
+};
+
+class ModrinthHasher : public Hasher {
+ public:
+ ModrinthHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("ModrinthHasher: %1").arg(file_path)); }
+
+ void executeTask() override;
+};
+
+Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
+Hasher::Ptr createFlameHasher(QString file_path);
+Hasher::Ptr createModrinthHasher(QString file_path);
+
+} // namespace Hashing
diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp
index 90edfe31..866e7540 100644
--- a/launcher/modplatform/helpers/NetworkModAPI.cpp
+++ b/launcher/modplatform/helpers/NetworkModAPI.cpp
@@ -31,48 +31,48 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
netJob->start();
}
-void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack)
+void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback)
{
auto response = new QByteArray();
auto job = getProject(pack.addonId.toString(), response);
- QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] {
+ QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
+ qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
- caller->infoRequestFinished(doc, pack);
+ callback(doc, pack);
});
job->start();
}
-void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const
+void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const
{
- auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network());
+ auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network());
auto response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
- QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, args] {
+ QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
+ qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
- caller->versionRequestSucceeded(doc, args.addonId);
+ callback(doc, args.addonId);
});
- QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] {
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] {
netJob->deleteLater();
delete response;
});
diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h
index 989bcec4..b8af22c7 100644
--- a/launcher/modplatform/helpers/NetworkModAPI.h
+++ b/launcher/modplatform/helpers/NetworkModAPI.h
@@ -5,8 +5,8 @@
class NetworkModAPI : public ModAPI {
public:
void searchMods(CallerType* caller, SearchArgs&& args) const override;
- void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override;
- void getVersions(CallerType* caller, VersionSearchArgs&& args) const override;
+ void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
+ void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override;
auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
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..3c15667c 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,193 @@
#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/BlockedModsDialog.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;
+ QList<QUrl> urls;
+ 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);
+ urls.append(QUrl(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 BlockedModsDialog(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,
+ urls);
+
+ 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 +236,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 +303,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 +343,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/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index 79d8edf7..e2d27547 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -2,11 +2,14 @@
#include "ModrinthAPI.h"
#include "ModrinthPackIndex.h"
-#include "FileSystem.h"
#include "Json.h"
#include "ModDownloadTask.h"
+#include "modplatform/helpers/HashUtils.h"
+
+#include "tasks/ConcurrentTask.h"
+
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -32,6 +35,8 @@ void ModrinthCheckUpdate::executeTask()
// Create all hashes
QStringList hashes;
auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10);
for (auto* mod : m_mods) {
if (!mod->enabled()) {
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
@@ -44,25 +49,25 @@ void ModrinthCheckUpdate::executeTask()
// 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());
+ auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
+ connect(hash_task.get(), &Task::succeeded, [&] {
+ QString hash (hash_task->getResult());
+ hashes.append(hash);
+ mappings.insert(hash, mod);
+ });
+ connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); });
+ hashing_task.addTask(hash_task);
+ } else {
+ hashes.append(hash);
+ mappings.insert(hash, mod);
}
-
- hashes.append(hash);
- mappings.insert(hash, mod);
}
+ QEventLoop loop;
+ connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); });
+ hashing_task.start();
+ loop.exec();
+
auto* response = new QByteArray();
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index e50dd96d..3e53becb 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -87,6 +87,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraData.donate.append(donate);
}
+ pack.extraData.body = Json::ensureString(obj, "body");
+
pack.extraDataLoaded = true;
}
diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp
deleted file mode 100644
index aa0c35df..00000000
--- a/launcher/modplatform/packwiz/Packwiz_test.cpp
+++ /dev/null
@@ -1,87 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
- * 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/>.
- */
-
-#include <QTemporaryDir>
-#include <QTest>
-
-#include "Packwiz.h"
-
-class PackwizTest : public QObject {
- Q_OBJECT
-
- private slots:
- // Files taken from https://github.com/packwiz/packwiz-example-pack
- void loadFromFile_Modrinth()
- {
- QString source = QFINDTESTDATA("testdata");
-
- QDir index_dir(source);
- 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, slug_mod);
-
- QVERIFY(metadata.isValid());
-
- QCOMPARE(metadata.name, "Borderless Mining");
- QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar");
- QCOMPARE(metadata.side, "client");
-
- QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar"));
- QCOMPARE(metadata.hash_format, "sha512");
- QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d");
-
- QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH);
- QCOMPARE(metadata.version(), "ug2qKTPR");
- QCOMPARE(metadata.mod_id(), "kYq5qkSL");
- }
-
- void loadFromFile_Curseforge()
- {
- QString source = QFINDTESTDATA("testdata");
-
- QDir index_dir(source);
- QString name_mod("screenshot-to-clipboard-fabric.pw.toml");
- QVERIFY(index_dir.entryList().contains(name_mod));
-
- // Try without the .pw.toml at the end
- name_mod.chop(8);
-
- auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod);
-
- QVERIFY(metadata.isValid());
-
- QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)");
- QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar");
- QCOMPARE(metadata.side, "both");
-
- QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar"));
- QCOMPARE(metadata.hash_format, "murmur2");
- QCOMPARE(metadata.hash, "1781245820");
-
- QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME);
- QCOMPARE(metadata.file_id, 3509043);
- QCOMPARE(metadata.project_id, 327154);
- }
-};
-
-QTEST_GUILESS_MAIN(PackwizTest)
-
-#include "Packwiz_test.moc"
diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
deleted file mode 100644
index 16545fd4..00000000
--- a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-name = "Borderless Mining"
-filename = "borderless-mining-1.1.1+1.18.jar"
-side = "client"
-
-[download]
-url = "https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar"
-hash-format = "sha512"
-hash = "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"
-
-[update]
-[update.modrinth]
-mod-id = "kYq5qkSL"
-version = "ug2qKTPR"
diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
deleted file mode 100644
index 87d70ada..00000000
--- a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-name = "Screenshot to Clipboard (Fabric)"
-filename = "screenshot-to-clipboard-1.0.7-fabric.jar"
-side = "both"
-
-[download]
-url = "https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar"
-hash-format = "murmur2"
-hash = "1781245820"
-
-[update]
-[update.curseforge]
-file-id = 3509043
-project-id = 327154
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/mojang/PackageManifest_test.cpp b/launcher/mojang/PackageManifest_test.cpp
deleted file mode 100644
index e8da4266..00000000
--- a/launcher/mojang/PackageManifest_test.cpp
+++ /dev/null
@@ -1,343 +0,0 @@
-#include <QTest>
-#include <QDebug>
-
-#include "mojang/PackageManifest.h"
-
-using namespace mojang_files;
-
-QDebug operator<<(QDebug debug, const Path &path)
-{
- debug << path.toString();
- return debug;
-}
-
-class PackageManifestTest : public QObject
-{
- Q_OBJECT
-
-private slots:
- void test_parse();
- void test_parse_file();
- void test_inspect();
-#ifndef Q_OS_WIN32
- void test_inspect_symlinks();
-#endif
- void mkdir_deep();
- void rmdir_deep();
-
- void identical_file();
- void changed_file();
- void added_file();
- void removed_file();
-};
-
-namespace {
-QByteArray basic_manifest = R"END(
-{
- "files": {
- "a/b.txt": {
- "type": "file",
- "downloads": {
- "raw": {
- "url": "http://dethware.org/b.txt",
- "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "size": 0
- }
- },
- "executable": true
- },
- "a/b/c": {
- "type": "directory"
- },
- "a/b/c.txt": {
- "type": "link",
- "target": "../b.txt"
- }
- }
-}
-)END";
-}
-
-void PackageManifestTest::test_parse()
-{
- auto manifest = Package::fromManifestContents(basic_manifest);
- QVERIFY(manifest.valid == true);
- QVERIFY(manifest.files.size() == 1);
- QVERIFY(manifest.files.count(Path("a/b.txt")));
- auto &file = manifest.files[Path("a/b.txt")];
- QVERIFY(file.executable == true);
- QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
- QVERIFY(file.size == 0);
- QVERIFY(manifest.folders.size() == 4);
- QVERIFY(manifest.folders.count(Path(".")));
- QVERIFY(manifest.folders.count(Path("a")));
- QVERIFY(manifest.folders.count(Path("a/b")));
- QVERIFY(manifest.folders.count(Path("a/b/c")));
- QVERIFY(manifest.symlinks.size() == 1);
- auto symlinkPath = Path("a/b/c.txt");
- QVERIFY(manifest.symlinks.count(symlinkPath));
- auto &symlink = manifest.symlinks[symlinkPath];
- QVERIFY(symlink == Path("../b.txt"));
- QVERIFY(manifest.sources.size() == 1);
-}
-
-void PackageManifestTest::test_parse_file() {
- auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json");
- auto manifest = Package::fromManifestFile(path);
- QVERIFY(manifest.valid == true);
-}
-
-
-void PackageManifestTest::test_inspect() {
- auto path = QFINDTESTDATA("testdata/inspect_win/");
- auto manifest = Package::fromInspectedFolder(path);
- QVERIFY(manifest.valid == true);
- QVERIFY(manifest.files.size() == 2);
- QVERIFY(manifest.files.count(Path("a/b.txt")));
- auto &file1 = manifest.files[Path("a/b.txt")];
- QVERIFY(file1.executable == false);
- QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
- QVERIFY(file1.size == 0);
- QVERIFY(manifest.files.count(Path("a/b/b.txt")));
- auto &file2 = manifest.files[Path("a/b/b.txt")];
- QVERIFY(file2.executable == false);
- QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
- QVERIFY(file2.size == 0);
- QVERIFY(manifest.folders.size() == 3);
- QVERIFY(manifest.folders.count(Path(".")));
- QVERIFY(manifest.folders.count(Path("a")));
- QVERIFY(manifest.folders.count(Path("a/b")));
- QVERIFY(manifest.symlinks.size() == 0);
-}
-
-#ifndef Q_OS_WIN32
-void PackageManifestTest::test_inspect_symlinks() {
- auto path = QFINDTESTDATA("testdata/inspect/");
- auto manifest = Package::fromInspectedFolder(path);
- QVERIFY(manifest.valid == true);
- QVERIFY(manifest.files.size() == 1);
- QVERIFY(manifest.files.count(Path("a/b.txt")));
- auto &file = manifest.files[Path("a/b.txt")];
- QVERIFY(file.executable == true);
- QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
- QVERIFY(file.size == 0);
- QVERIFY(manifest.folders.size() == 3);
- QVERIFY(manifest.folders.count(Path(".")));
- QVERIFY(manifest.folders.count(Path("a")));
- QVERIFY(manifest.folders.count(Path("a/b")));
- QVERIFY(manifest.symlinks.size() == 1);
- QVERIFY(manifest.symlinks.count(Path("a/b/b.txt")));
- qDebug() << manifest.symlinks[Path("a/b/b.txt")];
- QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt"));
-}
-#endif
-
-void PackageManifestTest::mkdir_deep() {
-
- Package from;
- auto to = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d/e": {
- "type": "directory"
- }
- }
-}
-)END");
- auto operations = UpdateOperations::resolve(from, to);
- QVERIFY(operations.deletes.size() == 0);
- QVERIFY(operations.rmdirs.size() == 0);
-
- QVERIFY(operations.mkdirs.size() == 6);
- QVERIFY(operations.mkdirs[0] == Path("."));
- QVERIFY(operations.mkdirs[1] == Path("a"));
- QVERIFY(operations.mkdirs[2] == Path("a/b"));
- QVERIFY(operations.mkdirs[3] == Path("a/b/c"));
- QVERIFY(operations.mkdirs[4] == Path("a/b/c/d"));
- QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e"));
-
- QVERIFY(operations.downloads.size() == 0);
- QVERIFY(operations.mklinks.size() == 0);
- QVERIFY(operations.executable_fixes.size() == 0);
-}
-
-void PackageManifestTest::rmdir_deep() {
-
- Package to;
- auto from = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d/e": {
- "type": "directory"
- }
- }
-}
-)END");
- auto operations = UpdateOperations::resolve(from, to);
- QVERIFY(operations.deletes.size() == 0);
-
- QVERIFY(operations.rmdirs.size() == 6);
- QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e"));
- QVERIFY(operations.rmdirs[1] == Path("a/b/c/d"));
- QVERIFY(operations.rmdirs[2] == Path("a/b/c"));
- QVERIFY(operations.rmdirs[3] == Path("a/b"));
- QVERIFY(operations.rmdirs[4] == Path("a"));
- QVERIFY(operations.rmdirs[5] == Path("."));
-
- QVERIFY(operations.mkdirs.size() == 0);
- QVERIFY(operations.downloads.size() == 0);
- QVERIFY(operations.mklinks.size() == 0);
- QVERIFY(operations.executable_fixes.size() == 0);
-}
-
-void PackageManifestTest::identical_file() {
- QByteArray manifest = R"END(
-{
- "files": {
- "a/b/c/d/empty.txt": {
- "type": "file",
- "downloads": {
- "raw": {
- "url": "http://dethware.org/empty.txt",
- "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "size": 0
- }
- },
- "executable": false
- }
- }
-}
-)END";
- auto from = Package::fromManifestContents(manifest);
- auto to = Package::fromManifestContents(manifest);
- auto operations = UpdateOperations::resolve(from, to);
- QVERIFY(operations.deletes.size() == 0);
- QVERIFY(operations.rmdirs.size() == 0);
- QVERIFY(operations.mkdirs.size() == 0);
- QVERIFY(operations.downloads.size() == 0);
- QVERIFY(operations.mklinks.size() == 0);
- QVERIFY(operations.executable_fixes.size() == 0);
-}
-
-void PackageManifestTest::changed_file() {
- auto from = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d/file": {
- "type": "file",
- "downloads": {
- "raw": {
- "url": "http://dethware.org/empty.txt",
- "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "size": 0
- }
- },
- "executable": false
- }
- }
-}
-)END");
- auto to = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d/file": {
- "type": "file",
- "downloads": {
- "raw": {
- "url": "http://dethware.org/space.txt",
- "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
- "size": 1
- }
- },
- "executable": false
- }
- }
-}
-)END");
- auto operations = UpdateOperations::resolve(from, to);
- QVERIFY(operations.deletes.size() == 1);
- QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
- QVERIFY(operations.rmdirs.size() == 0);
- QVERIFY(operations.mkdirs.size() == 0);
- QVERIFY(operations.downloads.size() == 1);
- QVERIFY(operations.mklinks.size() == 0);
- QVERIFY(operations.executable_fixes.size() == 0);
-}
-
-void PackageManifestTest::added_file() {
- auto from = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d": {
- "type": "directory"
- }
- }
-}
-)END");
- auto to = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d/file": {
- "type": "file",
- "downloads": {
- "raw": {
- "url": "http://dethware.org/space.txt",
- "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
- "size": 1
- }
- },
- "executable": false
- }
- }
-}
-)END");
- auto operations = UpdateOperations::resolve(from, to);
- QVERIFY(operations.deletes.size() == 0);
- QVERIFY(operations.rmdirs.size() == 0);
- QVERIFY(operations.mkdirs.size() == 0);
- QVERIFY(operations.downloads.size() == 1);
- QVERIFY(operations.mklinks.size() == 0);
- QVERIFY(operations.executable_fixes.size() == 0);
-}
-
-void PackageManifestTest::removed_file() {
- auto from = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d/file": {
- "type": "file",
- "downloads": {
- "raw": {
- "url": "http://dethware.org/space.txt",
- "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
- "size": 1
- }
- },
- "executable": false
- }
- }
-}
-)END");
- auto to = Package::fromManifestContents(R"END(
-{
- "files": {
- "a/b/c/d": {
- "type": "directory"
- }
- }
-}
-)END");
- auto operations = UpdateOperations::resolve(from, to);
- QVERIFY(operations.deletes.size() == 1);
- QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
- QVERIFY(operations.rmdirs.size() == 0);
- QVERIFY(operations.mkdirs.size() == 0);
- QVERIFY(operations.downloads.size() == 0);
- QVERIFY(operations.mklinks.size() == 0);
- QVERIFY(operations.executable_fixes.size() == 0);
-}
-
-QTEST_GUILESS_MAIN(PackageManifestTest)
-
-#include "PackageManifest_test.moc"
-
diff --git a/launcher/mojang/testdata/1.8.0_202-x64.json b/launcher/mojang/testdata/1.8.0_202-x64.json
deleted file mode 100644
index 3d99d719..00000000
--- a/launcher/mojang/testdata/1.8.0_202-x64.json
+++ /dev/null
@@ -1 +0,0 @@
-{"files": {"COPYRIGHT": {"downloads": {"lzma": {"sha1": "dd860e040807f7e53ae89da5f28dd73d57ac605d", "size": 1431, "url": "https://launcher.mojang.com/v1/objects/dd860e040807f7e53ae89da5f28dd73d57ac605d/COPYRIGHT"}, "raw": {"sha1": "c725183c757011e7ba96c83c1e86ee7e8b516a2b", "size": 3244, "url": "https://launcher.mojang.com/v1/objects/c725183c757011e7ba96c83c1e86ee7e8b516a2b/COPYRIGHT"}}, "executable": false, "type": "file"}, "LICENSE": {"downloads": {"raw": {"sha1": "3e86865deec0814c958bcf7fb87f790bccc0e8bd", "size": 40, "url": "https://launcher.mojang.com/v1/objects/3e86865deec0814c958bcf7fb87f790bccc0e8bd/LICENSE"}}, "executable": false, "type": "file"}, "README": {"downloads": {"raw": {"sha1": "f90331df1e5badeadc501d8dd70714c62a920204", "size": 46, "url": "https://launcher.mojang.com/v1/objects/f90331df1e5badeadc501d8dd70714c62a920204/README"}}, "executable": false, "type": "file"}, "THIRDPARTYLICENSEREADME-JAVAFX.txt": {"downloads": {"lzma": {"sha1": "4fee85109d7ff04b982d0576dabd15397f599125", "size": 15455, "url": "https://launcher.mojang.com/v1/objects/4fee85109d7ff04b982d0576dabd15397f599125/THIRDPARTYLICENSEREADME-JAVAFX.txt"}, "raw": {"sha1": "56ff42f87607b997b52ae0ef8bf315e36932e870", "size": 112724, "url": "https://launcher.mojang.com/v1/objects/56ff42f87607b997b52ae0ef8bf315e36932e870/THIRDPARTYLICENSEREADME-JAVAFX.txt"}}, "executable": false, "type": "file"}, "THIRDPARTYLICENSEREADME.txt": {"downloads": {"lzma": {"sha1": "419c1414ba46ae9dbfd38cf4e0601fff61644429", "size": 32266, "url": "https://launcher.mojang.com/v1/objects/419c1414ba46ae9dbfd38cf4e0601fff61644429/THIRDPARTYLICENSEREADME.txt"}, "raw": {"sha1": "b83c3f32261de3e48ccd20614a11e066b1ec9027", "size": 153824, "url": "https://launcher.mojang.com/v1/objects/b83c3f32261de3e48ccd20614a11e066b1ec9027/THIRDPARTYLICENSEREADME.txt"}}, "executable": false, "type": "file"}, "Welcome.html": {"downloads": {"lzma": {"sha1": "01c21a74b4aafb7cbe0388233c43cbdf77dcaaea", "size": 528, "url": "https://launcher.mojang.com/v1/objects/01c21a74b4aafb7cbe0388233c43cbdf77dcaaea/Welcome.html"}, "raw": {"sha1": "d98ae54f03dac87419abc19b97e315830c2da55f", "size": 955, "url": "https://launcher.mojang.com/v1/objects/d98ae54f03dac87419abc19b97e315830c2da55f/Welcome.html"}}, "executable": false, "type": "file"}, "bin": {"type": "directory"}, "bin/ControlPanel": {"target": "jcontrol", "type": "link"}, "bin/java": {"downloads": {"lzma": {"sha1": "3857eea1d59e1bc545c67a753ed2768254807b8a", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/3857eea1d59e1bc545c67a753ed2768254807b8a/java"}, "raw": {"sha1": "3d20560fb5d1a49cb689c2226972e92e06d27ba6", "size": 8464, "url": "https://launcher.mojang.com/v1/objects/3d20560fb5d1a49cb689c2226972e92e06d27ba6/java"}}, "executable": true, "type": "file"}, "bin/javaws": {"downloads": {"lzma": {"sha1": "a6bec5c049e76c4488294a256a2084ea23ddb440", "size": 38173, "url": "https://launcher.mojang.com/v1/objects/a6bec5c049e76c4488294a256a2084ea23ddb440/javaws"}, "raw": {"sha1": "955c0f0066e2f893b0c2b3ccd83e223722e4ab74", "size": 140296, "url": "https://launcher.mojang.com/v1/objects/955c0f0066e2f893b0c2b3ccd83e223722e4ab74/javaws"}}, "executable": true, "type": "file"}, "bin/jcontrol": {"downloads": {"lzma": {"sha1": "40c5e33748f252e1d950b579a4185ab2c23fc908", "size": 2166, "url": "https://launcher.mojang.com/v1/objects/40c5e33748f252e1d950b579a4185ab2c23fc908/jcontrol"}, "raw": {"sha1": "ed541733c8b51e34349c1f8010b277e58ad73f1e", "size": 6264, "url": "https://launcher.mojang.com/v1/objects/ed541733c8b51e34349c1f8010b277e58ad73f1e/jcontrol"}}, "executable": true, "type": "file"}, "bin/jjs": {"downloads": {"lzma": {"sha1": "d44d1ac421979f7671921986214812095a5b0e3b", "size": 2168, "url": "https://launcher.mojang.com/v1/objects/d44d1ac421979f7671921986214812095a5b0e3b/jjs"}, "raw": {"sha1": "f00f944c3dbe556793b5dc686aaeee3e5722e99b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/f00f944c3dbe556793b5dc686aaeee3e5722e99b/jjs"}}, "executable": true, "type": "file"}, "bin/keytool": {"downloads": {"lzma": {"sha1": "93c607dce450976667c382f609a367167bdec05c", "size": 2175, "url": "https://launcher.mojang.com/v1/objects/93c607dce450976667c382f609a367167bdec05c/keytool"}, "raw": {"sha1": "7114b561546270e441e9ed1bcc24e5188c068a42", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/7114b561546270e441e9ed1bcc24e5188c068a42/keytool"}}, "executable": true, "type": "file"}, "bin/orbd": {"downloads": {"lzma": {"sha1": "b27dfded5e2b2f6f02c555971c94e46ca14ac81b", "size": 2254, "url": "https://launcher.mojang.com/v1/objects/b27dfded5e2b2f6f02c555971c94e46ca14ac81b/orbd"}, "raw": {"sha1": "7f31217fecb3dbbd89f1dd3783fca58793a66fd2", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/7f31217fecb3dbbd89f1dd3783fca58793a66fd2/orbd"}}, "executable": true, "type": "file"}, "bin/pack200": {"downloads": {"lzma": {"sha1": "b52da4497b49b1508b6225a5740857ddb8f52e97", "size": 2183, "url": "https://launcher.mojang.com/v1/objects/b52da4497b49b1508b6225a5740857ddb8f52e97/pack200"}, "raw": {"sha1": "16ef3e801efb57e50bc6477a27a9d95d02d0775b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/16ef3e801efb57e50bc6477a27a9d95d02d0775b/pack200"}}, "executable": true, "type": "file"}, "bin/policytool": {"downloads": {"lzma": {"sha1": "87da4c07da45f3d1a1a9d732af197cd39bf69d10", "size": 2182, "url": "https://launcher.mojang.com/v1/objects/87da4c07da45f3d1a1a9d732af197cd39bf69d10/policytool"}, "raw": {"sha1": "a52a29424470cb9b8db5c2fb1751d0b697a7ec8e", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/a52a29424470cb9b8db5c2fb1751d0b697a7ec8e/policytool"}}, "executable": true, "type": "file"}, "bin/rmid": {"downloads": {"lzma": {"sha1": "1494c1174fde0c0a93ea117bc7edf7eb936c0512", "size": 2172, "url": "https://launcher.mojang.com/v1/objects/1494c1174fde0c0a93ea117bc7edf7eb936c0512/rmid"}, "raw": {"sha1": "5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8/rmid"}}, "executable": true, "type": "file"}, "bin/rmiregistry": {"downloads": {"lzma": {"sha1": "7070cf2ec5a5e520a880bae699431edf02083e7e", "size": 2174, "url": "https://launcher.mojang.com/v1/objects/7070cf2ec5a5e520a880bae699431edf02083e7e/rmiregistry"}, "raw": {"sha1": "5f518daa7050028d5d9d849634c73136f2b23a54", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/5f518daa7050028d5d9d849634c73136f2b23a54/rmiregistry"}}, "executable": true, "type": "file"}, "bin/servertool": {"downloads": {"lzma": {"sha1": "1db683a11cc9b7313426c84412f4d95be2fa7ccd", "size": 2185, "url": "https://launcher.mojang.com/v1/objects/1db683a11cc9b7313426c84412f4d95be2fa7ccd/servertool"}, "raw": {"sha1": "49d0ebfeb265ce5a8733e1014541ea2525674a60", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/49d0ebfeb265ce5a8733e1014541ea2525674a60/servertool"}}, "executable": true, "type": "file"}, "bin/tnameserv": {"downloads": {"lzma": {"sha1": "36da9c9a2c5a8b662a3f8d52ca67339bce1c2714", "size": 2291, "url": "https://launcher.mojang.com/v1/objects/36da9c9a2c5a8b662a3f8d52ca67339bce1c2714/tnameserv"}, "raw": {"sha1": "09d998f8efcb6f55d0d87f59e08f8b89662796d9", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/09d998f8efcb6f55d0d87f59e08f8b89662796d9/tnameserv"}}, "executable": true, "type": "file"}, "bin/unpack200": {"downloads": {"lzma": {"sha1": "344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2", "size": 79721, "url": "https://launcher.mojang.com/v1/objects/344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2/unpack200"}, "raw": {"sha1": "5dd933132f1b202e19e0c8e093f7113711cfdfc1", "size": 182616, "url": "https://launcher.mojang.com/v1/objects/5dd933132f1b202e19e0c8e093f7113711cfdfc1/unpack200"}}, "executable": true, "type": "file"}, "lib": {"type": "directory"}, "lib/amd64": {"type": "directory"}, "lib/amd64/jli": {"type": "directory"}, "lib/amd64/jli/libjli.so": {"downloads": {"lzma": {"sha1": "372331ee8e375888f798a2e88180a94493e141b0", "size": 48327, "url": "https://launcher.mojang.com/v1/objects/372331ee8e375888f798a2e88180a94493e141b0/libjli.so"}, "raw": {"sha1": "73b0cf8b7415686bc40c561ff77ff2740ccf7a44", "size": 108616, "url": "https://launcher.mojang.com/v1/objects/73b0cf8b7415686bc40c561ff77ff2740ccf7a44/libjli.so"}}, "executable": true, "type": "file"}, "lib/amd64/jvm.cfg": {"downloads": {"lzma": {"sha1": "86bcfebec37b38415525ffd77d3eaf70d0b1b4ca", "size": 435, "url": "https://launcher.mojang.com/v1/objects/86bcfebec37b38415525ffd77d3eaf70d0b1b4ca/jvm.cfg"}, "raw": {"sha1": "84b38bdc745de446ba0ca0232ea3aaf2efd721da", "size": 627, "url": "https://launcher.mojang.com/v1/objects/84b38bdc745de446ba0ca0232ea3aaf2efd721da/jvm.cfg"}}, "executable": false, "type": "file"}, "lib/amd64/libavplugin-53.so": {"downloads": {"lzma": {"sha1": "a332366762d9efc7b845a682b7edce62db44618c", "size": 14747, "url": "https://launcher.mojang.com/v1/objects/a332366762d9efc7b845a682b7edce62db44618c/libavplugin-53.so"}, "raw": {"sha1": "9bd1473dd8a0dc7950c7af1cc69a45548df26eb5", "size": 51720, "url": "https://launcher.mojang.com/v1/objects/9bd1473dd8a0dc7950c7af1cc69a45548df26eb5/libavplugin-53.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-54.so": {"downloads": {"lzma": {"sha1": "2c615852a0720a275163e00597c1f711f11341da", "size": 15153, "url": "https://launcher.mojang.com/v1/objects/2c615852a0720a275163e00597c1f711f11341da/libavplugin-54.so"}, "raw": {"sha1": "8808050c5949c4800b42d1b19b1f8b0d120bcacb", "size": 51768, "url": "https://launcher.mojang.com/v1/objects/8808050c5949c4800b42d1b19b1f8b0d120bcacb/libavplugin-54.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-55.so": {"downloads": {"lzma": {"sha1": "39ee8e7fe14f0010c78973962800f539c3e4c16b", "size": 15168, "url": "https://launcher.mojang.com/v1/objects/39ee8e7fe14f0010c78973962800f539c3e4c16b/libavplugin-55.so"}, "raw": {"sha1": "f10ea4ea3489e96d8d161a96790133c417ec44e1", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/f10ea4ea3489e96d8d161a96790133c417ec44e1/libavplugin-55.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-56.so": {"downloads": {"lzma": {"sha1": "abe7feced5a559f1bdc868526dc69484e0e591a0", "size": 15169, "url": "https://launcher.mojang.com/v1/objects/abe7feced5a559f1bdc868526dc69484e0e591a0/libavplugin-56.so"}, "raw": {"sha1": "e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed/libavplugin-56.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-57.so": {"downloads": {"lzma": {"sha1": "4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9", "size": 15174, "url": "https://launcher.mojang.com/v1/objects/4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9/libavplugin-57.so"}, "raw": {"sha1": "2949e7ff9b0ac90e8943c211cff141ab12eec3f8", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/2949e7ff9b0ac90e8943c211cff141ab12eec3f8/libavplugin-57.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-ffmpeg-56.so": {"downloads": {"lzma": {"sha1": "c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6", "size": 15231, "url": "https://launcher.mojang.com/v1/objects/c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6/libavplugin-ffmpeg-56.so"}, "raw": {"sha1": "0d36c971a9ad99fc2292092fdec3a4179b1021b9", "size": 51920, "url": "https://launcher.mojang.com/v1/objects/0d36c971a9ad99fc2292092fdec3a4179b1021b9/libavplugin-ffmpeg-56.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-ffmpeg-57.so": {"downloads": {"lzma": {"sha1": "087426bdbffebcfa372a438e863785f4ffbe9a6b", "size": 15180, "url": "https://launcher.mojang.com/v1/objects/087426bdbffebcfa372a438e863785f4ffbe9a6b/libavplugin-ffmpeg-57.so"}, "raw": {"sha1": "5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462/libavplugin-ffmpeg-57.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt.so": {"downloads": {"lzma": {"sha1": "018be58b205b73c842a55df811b70d0e8237216e", "size": 195720, "url": "https://launcher.mojang.com/v1/objects/018be58b205b73c842a55df811b70d0e8237216e/libawt.so"}, "raw": {"sha1": "02632cd326e3161c00a7e784599dd7b9ee053dce", "size": 759184, "url": "https://launcher.mojang.com/v1/objects/02632cd326e3161c00a7e784599dd7b9ee053dce/libawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt_headless.so": {"downloads": {"lzma": {"sha1": "7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa", "size": 11211, "url": "https://launcher.mojang.com/v1/objects/7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa/libawt_headless.so"}, "raw": {"sha1": "862157ec957008d0911c5daedc004b3a202623a4", "size": 39176, "url": "https://launcher.mojang.com/v1/objects/862157ec957008d0911c5daedc004b3a202623a4/libawt_headless.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt_xawt.so": {"downloads": {"lzma": {"sha1": "d536a96af27dfe35de6bb2c8759d51c488cdd8d4", "size": 149598, "url": "https://launcher.mojang.com/v1/objects/d536a96af27dfe35de6bb2c8759d51c488cdd8d4/libawt_xawt.so"}, "raw": {"sha1": "28232b3e01b6f11bfe098bfc6eafc3a513dcebf1", "size": 470232, "url": "https://launcher.mojang.com/v1/objects/28232b3e01b6f11bfe098bfc6eafc3a513dcebf1/libawt_xawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libbci.so": {"downloads": {"lzma": {"sha1": "c36fad091d11e64c815d5ca17c0ef7a55b0776b1", "size": 3509, "url": "https://launcher.mojang.com/v1/objects/c36fad091d11e64c815d5ca17c0ef7a55b0776b1/libbci.so"}, "raw": {"sha1": "33824051db1ccb6332e22c2b63231055240d0af0", "size": 12760, "url": "https://launcher.mojang.com/v1/objects/33824051db1ccb6332e22c2b63231055240d0af0/libbci.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdcpr.so": {"downloads": {"lzma": {"sha1": "70c6b0933a37f2b1124d6e7c131039241fe796ee", "size": 75969, "url": "https://launcher.mojang.com/v1/objects/70c6b0933a37f2b1124d6e7c131039241fe796ee/libdcpr.so"}, "raw": {"sha1": "fa7001bc5d80579e2716590f3eee8027da0beae7", "size": 204456, "url": "https://launcher.mojang.com/v1/objects/fa7001bc5d80579e2716590f3eee8027da0beae7/libdcpr.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdecora_sse.so": {"downloads": {"lzma": {"sha1": "514acc017dfb6cefaf8cc6d18006ce55781cc9bc", "size": 24397, "url": "https://launcher.mojang.com/v1/objects/514acc017dfb6cefaf8cc6d18006ce55781cc9bc/libdecora_sse.so"}, "raw": {"sha1": "d0c84233504c916e548e29f513e25f6a7479abfc", "size": 74912, "url": "https://launcher.mojang.com/v1/objects/d0c84233504c916e548e29f513e25f6a7479abfc/libdecora_sse.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdeploy.so": {"downloads": {"lzma": {"sha1": "6cf31fd98301c749ac0d2c7825f6d925a4409760", "size": 168999, "url": "https://launcher.mojang.com/v1/objects/6cf31fd98301c749ac0d2c7825f6d925a4409760/libdeploy.so"}, "raw": {"sha1": "b3832e97ed8ca794884b56a591b83d02a2c0c06f", "size": 642368, "url": "https://launcher.mojang.com/v1/objects/b3832e97ed8ca794884b56a591b83d02a2c0c06f/libdeploy.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdt_socket.so": {"downloads": {"lzma": {"sha1": "4cc5c880dbb6fa180436d12d60f0abec8ebb59dc", "size": 7784, "url": "https://launcher.mojang.com/v1/objects/4cc5c880dbb6fa180436d12d60f0abec8ebb59dc/libdt_socket.so"}, "raw": {"sha1": "91ce96f252b8139fc12f0f224ed5b1a041767ab7", "size": 24616, "url": "https://launcher.mojang.com/v1/objects/91ce96f252b8139fc12f0f224ed5b1a041767ab7/libdt_socket.so"}}, "executable": true, "type": "file"}, "lib/amd64/libfontmanager.so": {"downloads": {"lzma": {"sha1": "f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d", "size": 146951, "url": "https://launcher.mojang.com/v1/objects/f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d/libfontmanager.so"}, "raw": {"sha1": "2428e805f2c53d1283a033dfd11a86fbb7bd7159", "size": 490672, "url": "https://launcher.mojang.com/v1/objects/2428e805f2c53d1283a033dfd11a86fbb7bd7159/libfontmanager.so"}}, "executable": true, "type": "file"}, "lib/amd64/libfxplugins.so": {"downloads": {"lzma": {"sha1": "a640143365d382a5ad743a784bc2f3706d9d6d67", "size": 50048, "url": "https://launcher.mojang.com/v1/objects/a640143365d382a5ad743a784bc2f3706d9d6d67/libfxplugins.so"}, "raw": {"sha1": "0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee", "size": 151448, "url": "https://launcher.mojang.com/v1/objects/0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee/libfxplugins.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglass.so": {"downloads": {"lzma": {"sha1": "f1ff517714fa5f2c861f33b32db823fe851541f1", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/f1ff517714fa5f2c861f33b32db823fe851541f1/libglass.so"}, "raw": {"sha1": "e7f4fece30ac727be8148d33b8256abd3a41cef9", "size": 13072, "url": "https://launcher.mojang.com/v1/objects/e7f4fece30ac727be8148d33b8256abd3a41cef9/libglass.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglassgtk2.so": {"downloads": {"lzma": {"sha1": "15b90f7a2baacd15e80aa9785d87cf1e4258376d", "size": 220476, "url": "https://launcher.mojang.com/v1/objects/15b90f7a2baacd15e80aa9785d87cf1e4258376d/libglassgtk2.so"}, "raw": {"sha1": "e30a634c2ff2143bdee512360553d6e0304f33b2", "size": 844984, "url": "https://launcher.mojang.com/v1/objects/e30a634c2ff2143bdee512360553d6e0304f33b2/libglassgtk2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglassgtk3.so": {"downloads": {"lzma": {"sha1": "868c231165f8c9043b7f0e7de208ec023f06a6e7", "size": 220560, "url": "https://launcher.mojang.com/v1/objects/868c231165f8c9043b7f0e7de208ec023f06a6e7/libglassgtk3.so"}, "raw": {"sha1": "762a11a2b376b7b5a2a7cad780715524fdd176d5", "size": 845304, "url": "https://launcher.mojang.com/v1/objects/762a11a2b376b7b5a2a7cad780715524fdd176d5/libglassgtk3.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglib-lite.so": {"downloads": {"lzma": {"sha1": "61b8871242febe1be262de167dc20ae94bf964b4", "size": 457046, "url": "https://launcher.mojang.com/v1/objects/61b8871242febe1be262de167dc20ae94bf964b4/libglib-lite.so"}, "raw": {"sha1": "63afa060fc3f120af76128e51d32603fc4336fa8", "size": 1538352, "url": "https://launcher.mojang.com/v1/objects/63afa060fc3f120af76128e51d32603fc4336fa8/libglib-lite.so"}}, "executable": true, "type": "file"}, "lib/amd64/libgstreamer-lite.so": {"downloads": {"lzma": {"sha1": "2447dc368406ba1b989a29937d41924620e01988", "size": 673056, "url": "https://launcher.mojang.com/v1/objects/2447dc368406ba1b989a29937d41924620e01988/libgstreamer-lite.so"}, "raw": {"sha1": "5505e7ca592ac64371d3db8fe53bcb602e9723d3", "size": 2263872, "url": "https://launcher.mojang.com/v1/objects/5505e7ca592ac64371d3db8fe53bcb602e9723d3/libgstreamer-lite.so"}}, "executable": true, "type": "file"}, "lib/amd64/libhprof.so": {"downloads": {"lzma": {"sha1": "94a5589c818db1fb1cf1881e24e217c309fce2e4", "size": 64471, "url": "https://launcher.mojang.com/v1/objects/94a5589c818db1fb1cf1881e24e217c309fce2e4/libhprof.so"}, "raw": {"sha1": "4bb9bdeef6133b6dd558d52d691b077c03e9b0ee", "size": 175504, "url": "https://launcher.mojang.com/v1/objects/4bb9bdeef6133b6dd558d52d691b077c03e9b0ee/libhprof.so"}}, "executable": true, "type": "file"}, "lib/amd64/libinstrument.so": {"downloads": {"lzma": {"sha1": "84ffea356caf725b42c86a8ebc9587f477ddde29", "size": 18603, "url": "https://launcher.mojang.com/v1/objects/84ffea356caf725b42c86a8ebc9587f477ddde29/libinstrument.so"}, "raw": {"sha1": "cb8009769601e3fecd7ea2b36c344f737b1a9da7", "size": 51560, "url": "https://launcher.mojang.com/v1/objects/cb8009769601e3fecd7ea2b36c344f737b1a9da7/libinstrument.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2gss.so": {"downloads": {"lzma": {"sha1": "4b2aa699506b126098b585a9617ce1c05707fa29", "size": 14132, "url": "https://launcher.mojang.com/v1/objects/4b2aa699506b126098b585a9617ce1c05707fa29/libj2gss.so"}, "raw": {"sha1": "cbce4a302b255d4d1924ef7606f038af766c5e86", "size": 47688, "url": "https://launcher.mojang.com/v1/objects/cbce4a302b255d4d1924ef7606f038af766c5e86/libj2gss.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2pcsc.so": {"downloads": {"lzma": {"sha1": "2361d3b2e3da48593c391b29b0d2b5409e4c55e5", "size": 5074, "url": "https://launcher.mojang.com/v1/objects/2361d3b2e3da48593c391b29b0d2b5409e4c55e5/libj2pcsc.so"}, "raw": {"sha1": "1274178492e7a3e997e12f67794616f7c3d8d0b9", "size": 18296, "url": "https://launcher.mojang.com/v1/objects/1274178492e7a3e997e12f67794616f7c3d8d0b9/libj2pcsc.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2pkcs11.so": {"downloads": {"lzma": {"sha1": "ef927e2790ba05931d0f0bdd63da3d275a834946", "size": 21573, "url": "https://launcher.mojang.com/v1/objects/ef927e2790ba05931d0f0bdd63da3d275a834946/libj2pkcs11.so"}, "raw": {"sha1": "bd4f2af9bfdc6168633d1920c1a1415de06bb45a", "size": 79472, "url": "https://launcher.mojang.com/v1/objects/bd4f2af9bfdc6168633d1920c1a1415de06bb45a/libj2pkcs11.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjaas_unix.so": {"downloads": {"lzma": {"sha1": "7f7e843544ee1eb1454a5826bdd4218685b79430", "size": 2404, "url": "https://launcher.mojang.com/v1/objects/7f7e843544ee1eb1454a5826bdd4218685b79430/libjaas_unix.so"}, "raw": {"sha1": "4c517925c7d464a5b719898eb0bea1b04df31f1f", "size": 8192, "url": "https://launcher.mojang.com/v1/objects/4c517925c7d464a5b719898eb0bea1b04df31f1f/libjaas_unix.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjava.so": {"downloads": {"lzma": {"sha1": "5eee7a42600a44a8bb8d6d7f510fd96a29637ac0", "size": 63113, "url": "https://launcher.mojang.com/v1/objects/5eee7a42600a44a8bb8d6d7f510fd96a29637ac0/libjava.so"}, "raw": {"sha1": "e280aeddf3fc0ec664aef7efc0e0e197a54aaf02", "size": 227672, "url": "https://launcher.mojang.com/v1/objects/e280aeddf3fc0ec664aef7efc0e0e197a54aaf02/libjava.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjava_crw_demo.so": {"downloads": {"lzma": {"sha1": "b197cf23ae3556eb0b45c663f0a8cb62408b961e", "size": 10412, "url": "https://launcher.mojang.com/v1/objects/b197cf23ae3556eb0b45c663f0a8cb62408b961e/libjava_crw_demo.so"}, "raw": {"sha1": "18f20f906977c90d0090b41dbda8dd5cfead5a4c", "size": 26144, "url": "https://launcher.mojang.com/v1/objects/18f20f906977c90d0090b41dbda8dd5cfead5a4c/libjava_crw_demo.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font.so": {"downloads": {"lzma": {"sha1": "ffbba0e5022f829412b86063d8a90f95f16709b1", "size": 5608, "url": "https://launcher.mojang.com/v1/objects/ffbba0e5022f829412b86063d8a90f95f16709b1/libjavafx_font.so"}, "raw": {"sha1": "8634a0aca612fc40420a4a7cc8af4cc46cfc6725", "size": 17104, "url": "https://launcher.mojang.com/v1/objects/8634a0aca612fc40420a4a7cc8af4cc46cfc6725/libjavafx_font.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_freetype.so": {"downloads": {"lzma": {"sha1": "310271eda8a2ac264ffc3640a9d847b49438d0bd", "size": 6942, "url": "https://launcher.mojang.com/v1/objects/310271eda8a2ac264ffc3640a9d847b49438d0bd/libjavafx_font_freetype.so"}, "raw": {"sha1": "3e7572d047c12ba2bc43acec7f98a67c20af8042", "size": 27616, "url": "https://launcher.mojang.com/v1/objects/3e7572d047c12ba2bc43acec7f98a67c20af8042/libjavafx_font_freetype.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_pango.so": {"downloads": {"lzma": {"sha1": "a7bcf0669e70b0f43099a99c81e6b6440cb40ac0", "size": 5820, "url": "https://launcher.mojang.com/v1/objects/a7bcf0669e70b0f43099a99c81e6b6440cb40ac0/libjavafx_font_pango.so"}, "raw": {"sha1": "f0b775cc9a514c7ee8b4d6fb300653ce548caf10", "size": 25560, "url": "https://launcher.mojang.com/v1/objects/f0b775cc9a514c7ee8b4d6fb300653ce548caf10/libjavafx_font_pango.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_t2k.so": {"downloads": {"lzma": {"sha1": "551c29dc7c7fc83223aa36a6187f7e0c5d650538", "size": 431450, "url": "https://launcher.mojang.com/v1/objects/551c29dc7c7fc83223aa36a6187f7e0c5d650538/libjavafx_font_t2k.so"}, "raw": {"sha1": "91e5813057c3b852d411540160f8ad05fb9f1ed3", "size": 1486128, "url": "https://launcher.mojang.com/v1/objects/91e5813057c3b852d411540160f8ad05fb9f1ed3/libjavafx_font_t2k.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_iio.so": {"downloads": {"lzma": {"sha1": "c832998fd5e06ed6dcd6428816194c350785420c", "size": 101479, "url": "https://launcher.mojang.com/v1/objects/c832998fd5e06ed6dcd6428816194c350785420c/libjavafx_iio.so"}, "raw": {"sha1": "dcdf68cb25677b76c1cf0bb94294e6e9880a6678", "size": 256336, "url": "https://launcher.mojang.com/v1/objects/dcdf68cb25677b76c1cf0bb94294e6e9880a6678/libjavafx_iio.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjawt.so": {"downloads": {"lzma": {"sha1": "c1ced6aad5c69ff444dc67d0fd7e333558953831", "size": 1872, "url": "https://launcher.mojang.com/v1/objects/c1ced6aad5c69ff444dc67d0fd7e333558953831/libjawt.so"}, "raw": {"sha1": "c5032f2c6fa40bea24e56605cf76b26a27e87b67", "size": 8048, "url": "https://launcher.mojang.com/v1/objects/c5032f2c6fa40bea24e56605cf76b26a27e87b67/libjawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjdwp.so": {"downloads": {"lzma": {"sha1": "c1aabbb3f5a624b9ad10ed871a1d83510a99b646", "size": 94884, "url": "https://launcher.mojang.com/v1/objects/c1aabbb3f5a624b9ad10ed871a1d83510a99b646/libjdwp.so"}, "raw": {"sha1": "a043e97be47937f6f552e94cf79c76c1c57f9594", "size": 272248, "url": "https://launcher.mojang.com/v1/objects/a043e97be47937f6f552e94cf79c76c1c57f9594/libjdwp.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfr.so": {"downloads": {"lzma": {"sha1": "11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7", "size": 8780, "url": "https://launcher.mojang.com/v1/objects/11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7/libjfr.so"}, "raw": {"sha1": "312392dd186b11c418183e818f1928e8685a07e5", "size": 28384, "url": "https://launcher.mojang.com/v1/objects/312392dd186b11c418183e818f1928e8685a07e5/libjfr.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfxmedia.so": {"downloads": {"lzma": {"sha1": "a4e7a126eb648ce6e5e6dc151831da37d8334139", "size": 391897, "url": "https://launcher.mojang.com/v1/objects/a4e7a126eb648ce6e5e6dc151831da37d8334139/libjfxmedia.so"}, "raw": {"sha1": "5fa54944327a6012c3d34cb5c1c4432762178dc8", "size": 1636376, "url": "https://launcher.mojang.com/v1/objects/5fa54944327a6012c3d34cb5c1c4432762178dc8/libjfxmedia.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfxwebkit.so": {"downloads": {"lzma": {"sha1": "b274debd222cdcc2ee84160ebb95144b3880bc97", "size": 20492825, "url": "https://launcher.mojang.com/v1/objects/b274debd222cdcc2ee84160ebb95144b3880bc97/libjfxwebkit.so"}, "raw": {"sha1": "ecee564c3b2f645131b35bb3004abd4caeabd291", "size": 91014584, "url": "https://launcher.mojang.com/v1/objects/ecee564c3b2f645131b35bb3004abd4caeabd291/libjfxwebkit.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjpeg.so": {"downloads": {"lzma": {"sha1": "9ad55e370c5eaaa73c3158339db3c368b1aaf0cb", "size": 113072, "url": "https://launcher.mojang.com/v1/objects/9ad55e370c5eaaa73c3158339db3c368b1aaf0cb/libjpeg.so"}, "raw": {"sha1": "651e6d53ae67db1f0efbf7f104447a9b49b7e333", "size": 292520, "url": "https://launcher.mojang.com/v1/objects/651e6d53ae67db1f0efbf7f104447a9b49b7e333/libjpeg.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsdt.so": {"downloads": {"lzma": {"sha1": "04b6d1361a34c496b5f652b2477784d69b8b6baf", "size": 3964, "url": "https://launcher.mojang.com/v1/objects/04b6d1361a34c496b5f652b2477784d69b8b6baf/libjsdt.so"}, "raw": {"sha1": "82b48a82bf6183d34cf00a0f81661b45c616f31b", "size": 12904, "url": "https://launcher.mojang.com/v1/objects/82b48a82bf6183d34cf00a0f81661b45c616f31b/libjsdt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsig.so": {"downloads": {"lzma": {"sha1": "37d3b89abde397216cc4ecb1339d8543d99b8428", "size": 3536, "url": "https://launcher.mojang.com/v1/objects/37d3b89abde397216cc4ecb1339d8543d99b8428/libjsig.so"}, "raw": {"sha1": "42e52ba1bcbe0362ab24bcf65c93797354db6fb9", "size": 13336, "url": "https://launcher.mojang.com/v1/objects/42e52ba1bcbe0362ab24bcf65c93797354db6fb9/libjsig.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsound.so": {"downloads": {"lzma": {"sha1": "7e3c565d74d8ffae716f32b05544fa4a6f108adc", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7e3c565d74d8ffae716f32b05544fa4a6f108adc/libjsound.so"}, "raw": {"sha1": "0c0fc63b92d7b83c9960fa80d45c80553ea20254", "size": 8232, "url": "https://launcher.mojang.com/v1/objects/0c0fc63b92d7b83c9960fa80d45c80553ea20254/libjsound.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsoundalsa.so": {"downloads": {"lzma": {"sha1": "b06c51858a25ff776519495f1b9b3d9f604b089f", "size": 23097, "url": "https://launcher.mojang.com/v1/objects/b06c51858a25ff776519495f1b9b3d9f604b089f/libjsoundalsa.so"}, "raw": {"sha1": "281d37f0326d4a12dc7ea316ead09c198ff7bdf7", "size": 83256, "url": "https://launcher.mojang.com/v1/objects/281d37f0326d4a12dc7ea316ead09c198ff7bdf7/libjsoundalsa.so"}}, "executable": true, "type": "file"}, "lib/amd64/liblcms.so": {"downloads": {"lzma": {"sha1": "7a239baba2086cae49114b382b74b971da02f08e", "size": 176175, "url": "https://launcher.mojang.com/v1/objects/7a239baba2086cae49114b382b74b971da02f08e/liblcms.so"}, "raw": {"sha1": "c8895cc3c3d023d9e059225969ab67954772c0a1", "size": 526872, "url": "https://launcher.mojang.com/v1/objects/c8895cc3c3d023d9e059225969ab67954772c0a1/liblcms.so"}}, "executable": true, "type": "file"}, "lib/amd64/libmanagement.so": {"downloads": {"lzma": {"sha1": "aed3fdbcefd1716abfc6a306687c8b741cbb318e", "size": 12838, "url": "https://launcher.mojang.com/v1/objects/aed3fdbcefd1716abfc6a306687c8b741cbb318e/libmanagement.so"}, "raw": {"sha1": "eba35f61e0d50e30874b7c7b335edf2d52662423", "size": 51808, "url": "https://launcher.mojang.com/v1/objects/eba35f61e0d50e30874b7c7b335edf2d52662423/libmanagement.so"}}, "executable": true, "type": "file"}, "lib/amd64/libmlib_image.so": {"downloads": {"lzma": {"sha1": "1bb181f079492d55c7a458e96488cd17fe0a7b86", "size": 310272, "url": "https://launcher.mojang.com/v1/objects/1bb181f079492d55c7a458e96488cd17fe0a7b86/libmlib_image.so"}, "raw": {"sha1": "c973c450d33873675945d4694be484e3427f58f1", "size": 1048136, "url": "https://launcher.mojang.com/v1/objects/c973c450d33873675945d4694be484e3427f58f1/libmlib_image.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnet.so": {"downloads": {"lzma": {"sha1": "9dd79703b6deb86e0321afe01c6ac508263c8312", "size": 38123, "url": "https://launcher.mojang.com/v1/objects/9dd79703b6deb86e0321afe01c6ac508263c8312/libnet.so"}, "raw": {"sha1": "b3a17b7d53fcdf1e689e1ec29ce851eee6022ead", "size": 116920, "url": "https://launcher.mojang.com/v1/objects/b3a17b7d53fcdf1e689e1ec29ce851eee6022ead/libnet.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnio.so": {"downloads": {"lzma": {"sha1": "5697c89d5d5d9b74f2e1555fcbba79dd4049e287", "size": 24445, "url": "https://launcher.mojang.com/v1/objects/5697c89d5d5d9b74f2e1555fcbba79dd4049e287/libnio.so"}, "raw": {"sha1": "573bf8f64dbcc397f8abd3e1da28f90ab0679f5b", "size": 93872, "url": "https://launcher.mojang.com/v1/objects/573bf8f64dbcc397f8abd3e1da28f90ab0679f5b/libnio.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnpjp2.so": {"downloads": {"lzma": {"sha1": "6fe53b5951ff740e7f2ef7ffe5975af26da06718", "size": 57892, "url": "https://launcher.mojang.com/v1/objects/6fe53b5951ff740e7f2ef7ffe5975af26da06718/libnpjp2.so"}, "raw": {"sha1": "2bb13c53a4280379253475e51216b97eed1d4ce3", "size": 216592, "url": "https://launcher.mojang.com/v1/objects/2bb13c53a4280379253475e51216b97eed1d4ce3/libnpjp2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnpt.so": {"downloads": {"lzma": {"sha1": "1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876", "size": 5070, "url": "https://launcher.mojang.com/v1/objects/1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876/libnpt.so"}, "raw": {"sha1": "6b1ff6b9b4624f3cc7801f221c82b8046fb76364", "size": 17504, "url": "https://launcher.mojang.com/v1/objects/6b1ff6b9b4624f3cc7801f221c82b8046fb76364/libnpt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_common.so": {"downloads": {"lzma": {"sha1": "f4aca04c90bc7505851c074a08b2c31cae1f94fa", "size": 23315, "url": "https://launcher.mojang.com/v1/objects/f4aca04c90bc7505851c074a08b2c31cae1f94fa/libprism_common.so"}, "raw": {"sha1": "b00866b6ed8646a29a334d46e297267552f27de8", "size": 59008, "url": "https://launcher.mojang.com/v1/objects/b00866b6ed8646a29a334d46e297267552f27de8/libprism_common.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_es2.so": {"downloads": {"lzma": {"sha1": "7ff4173c338c7a6f370f231670055737e032da3e", "size": 18416, "url": "https://launcher.mojang.com/v1/objects/7ff4173c338c7a6f370f231670055737e032da3e/libprism_es2.so"}, "raw": {"sha1": "1390a1dc14345e5a948148e59195d62f3a83863f", "size": 63808, "url": "https://launcher.mojang.com/v1/objects/1390a1dc14345e5a948148e59195d62f3a83863f/libprism_es2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_sw.so": {"downloads": {"lzma": {"sha1": "6728e8bf7b214067d715be6fe0325910d63c2468", "size": 29457, "url": "https://launcher.mojang.com/v1/objects/6728e8bf7b214067d715be6fe0325910d63c2468/libprism_sw.so"}, "raw": {"sha1": "7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4", "size": 71608, "url": "https://launcher.mojang.com/v1/objects/7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4/libprism_sw.so"}}, "executable": true, "type": "file"}, "lib/amd64/libresource.so": {"downloads": {"lzma": {"sha1": "1e35e63f1e74915fba620f1febf420b919d49bc5", "size": 2633, "url": "https://launcher.mojang.com/v1/objects/1e35e63f1e74915fba620f1febf420b919d49bc5/libresource.so"}, "raw": {"sha1": "57490353ad0d83ab1930355213dea269795434fe", "size": 13456, "url": "https://launcher.mojang.com/v1/objects/57490353ad0d83ab1930355213dea269795434fe/libresource.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsctp.so": {"downloads": {"lzma": {"sha1": "4340132ed250d7849a016e071be773eaedd33aa8", "size": 9332, "url": "https://launcher.mojang.com/v1/objects/4340132ed250d7849a016e071be773eaedd33aa8/libsctp.so"}, "raw": {"sha1": "4a80e743750f127c0d5a359f5cd60b97e7ee12ae", "size": 29552, "url": "https://launcher.mojang.com/v1/objects/4a80e743750f127c0d5a359f5cd60b97e7ee12ae/libsctp.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsplashscreen.so": {"downloads": {"lzma": {"sha1": "b226c8dbd73a548fc2b042ee6db6cc80e727597c", "size": 190305, "url": "https://launcher.mojang.com/v1/objects/b226c8dbd73a548fc2b042ee6db6cc80e727597c/libsplashscreen.so"}, "raw": {"sha1": "87d6a491f5ba7e6c4d972264a0c9063afea567a2", "size": 441376, "url": "https://launcher.mojang.com/v1/objects/87d6a491f5ba7e6c4d972264a0c9063afea567a2/libsplashscreen.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsunec.so": {"downloads": {"lzma": {"sha1": "6ebba98fab1e3d872d1363235b76497f6f9babdc", "size": 88829, "url": "https://launcher.mojang.com/v1/objects/6ebba98fab1e3d872d1363235b76497f6f9babdc/libsunec.so"}, "raw": {"sha1": "3b262a0a530f6e4e539aed2cd27b4de1d0ed8859", "size": 283368, "url": "https://launcher.mojang.com/v1/objects/3b262a0a530f6e4e539aed2cd27b4de1d0ed8859/libsunec.so"}}, "executable": true, "type": "file"}, "lib/amd64/libt2k.so": {"downloads": {"lzma": {"sha1": "602cb812ef0b350ccf56ce209a260ddbe3743d92", "size": 190720, "url": "https://launcher.mojang.com/v1/objects/602cb812ef0b350ccf56ce209a260ddbe3743d92/libt2k.so"}, "raw": {"sha1": "b072c56df997f61e15e6b5a43b8907b0d25c2043", "size": 504840, "url": "https://launcher.mojang.com/v1/objects/b072c56df997f61e15e6b5a43b8907b0d25c2043/libt2k.so"}}, "executable": true, "type": "file"}, "lib/amd64/libunpack.so": {"downloads": {"lzma": {"sha1": "7107b615e941074f0b14c31c88fb67200aacd37f", "size": 70308, "url": "https://launcher.mojang.com/v1/objects/7107b615e941074f0b14c31c88fb67200aacd37f/libunpack.so"}, "raw": {"sha1": "b05ff862ed87928ed91e80e5604673c5ea710a53", "size": 197712, "url": "https://launcher.mojang.com/v1/objects/b05ff862ed87928ed91e80e5604673c5ea710a53/libunpack.so"}}, "executable": true, "type": "file"}, "lib/amd64/libverify.so": {"downloads": {"lzma": {"sha1": "ecd98efb8c7da441a8c3580e8f5598f3cb4165b1", "size": 21885, "url": "https://launcher.mojang.com/v1/objects/ecd98efb8c7da441a8c3580e8f5598f3cb4165b1/libverify.so"}, "raw": {"sha1": "e2c8d92531c45ab9be69ffb72c87fa12e9e59827", "size": 66112, "url": "https://launcher.mojang.com/v1/objects/e2c8d92531c45ab9be69ffb72c87fa12e9e59827/libverify.so"}}, "executable": true, "type": "file"}, "lib/amd64/libzip.so": {"downloads": {"lzma": {"sha1": "7c562342e3f7b138dc978495447e3e6a96c2cf45", "size": 54876, "url": "https://launcher.mojang.com/v1/objects/7c562342e3f7b138dc978495447e3e6a96c2cf45/libzip.so"}, "raw": {"sha1": "5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f", "size": 127016, "url": "https://launcher.mojang.com/v1/objects/5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f/libzip.so"}}, "executable": true, "type": "file"}, "lib/amd64/server": {"type": "directory"}, "lib/amd64/server/Xusage.txt": {"downloads": {"lzma": {"sha1": "acb2da24a4c765887df83985e4c26d6be302a0a3", "size": 629, "url": "https://launcher.mojang.com/v1/objects/acb2da24a4c765887df83985e4c26d6be302a0a3/Xusage.txt"}, "raw": {"sha1": "6983727eafe140f9dd793c78aa6f3e007438243a", "size": 1423, "url": "https://launcher.mojang.com/v1/objects/6983727eafe140f9dd793c78aa6f3e007438243a/Xusage.txt"}}, "executable": false, "type": "file"}, "lib/amd64/server/libjsig.so": {"target": "../libjsig.so", "type": "link"}, "lib/amd64/server/libjvm.so": {"downloads": {"lzma": {"sha1": "d5c6f3338aaa6712f79d680ac8c3e31beebaa886", "size": 4154311, "url": "https://launcher.mojang.com/v1/objects/d5c6f3338aaa6712f79d680ac8c3e31beebaa886/libjvm.so"}, "raw": {"sha1": "23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca", "size": 17045016, "url": "https://launcher.mojang.com/v1/objects/23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca/libjvm.so"}}, "executable": true, "type": "file"}, "lib/applet": {"type": "directory"}, "lib/calendars.properties": {"downloads": {"lzma": {"sha1": "4a757c23f2942bd802a4f80235131146d9267750", "size": 558, "url": "https://launcher.mojang.com/v1/objects/4a757c23f2942bd802a4f80235131146d9267750/calendars.properties"}, "raw": {"sha1": "42ebb0988124433b8f2a6e5d9a74ed41240bcfc6", "size": 1378, "url": "https://launcher.mojang.com/v1/objects/42ebb0988124433b8f2a6e5d9a74ed41240bcfc6/calendars.properties"}}, "executable": false, "type": "file"}, "lib/charsets.jar": {"downloads": {"lzma": {"sha1": "2bf44143b2ad9d7d55045a4de4a562330c194dc0", "size": 412367, "url": "https://launcher.mojang.com/v1/objects/2bf44143b2ad9d7d55045a4de4a562330c194dc0/charsets.jar"}, "raw": {"sha1": "d73ab9f8de255a7e112ddd13622bf7f6b18c8447", "size": 3135615, "url": "https://launcher.mojang.com/v1/objects/d73ab9f8de255a7e112ddd13622bf7f6b18c8447/charsets.jar"}}, "executable": false, "type": "file"}, "lib/classlist": {"downloads": {"lzma": {"sha1": "14e7c73d21b8513b0aff8d86e5cb34c52021fbca", "size": 15024, "url": "https://launcher.mojang.com/v1/objects/14e7c73d21b8513b0aff8d86e5cb34c52021fbca/classlist"}, "raw": {"sha1": "9c0404b63c87e2fed35e3a6cd137d6cf876c42bd", "size": 84311, "url": "https://launcher.mojang.com/v1/objects/9c0404b63c87e2fed35e3a6cd137d6cf876c42bd/classlist"}}, "executable": false, "type": "file"}, "lib/cmm": {"type": "directory"}, "lib/cmm/CIEXYZ.pf": {"downloads": {"lzma": {"sha1": "fcc5ca2fd3f45cac3434b480fa3ce00007e96529", "size": 8964, "url": "https://launcher.mojang.com/v1/objects/fcc5ca2fd3f45cac3434b480fa3ce00007e96529/CIEXYZ.pf"}, "raw": {"sha1": "b7779924c70554647b87c2a86159ca7781e929f8", "size": 51236, "url": "https://launcher.mojang.com/v1/objects/b7779924c70554647b87c2a86159ca7781e929f8/CIEXYZ.pf"}}, "executable": false, "type": "file"}, "lib/cmm/GRAY.pf": {"downloads": {"lzma": {"sha1": "5388ccfe67d3131d6d02143d8e8895003ab14ff6", "size": 299, "url": "https://launcher.mojang.com/v1/objects/5388ccfe67d3131d6d02143d8e8895003ab14ff6/GRAY.pf"}, "raw": {"sha1": "27f93961d66b8230d0cdb8b166bc8b4153d5bc2d", "size": 632, "url": "https://launcher.mojang.com/v1/objects/27f93961d66b8230d0cdb8b166bc8b4153d5bc2d/GRAY.pf"}}, "executable": false, "type": "file"}, "lib/cmm/LINEAR_RGB.pf": {"downloads": {"lzma": {"sha1": "2bd90f09c8deb64b1729d6b8173c78f9e9cab27b", "size": 678, "url": "https://launcher.mojang.com/v1/objects/2bd90f09c8deb64b1729d6b8173c78f9e9cab27b/LINEAR_RGB.pf"}, "raw": {"sha1": "7913274c2f73bafcf888f09ff60990b100214ede", "size": 1044, "url": "https://launcher.mojang.com/v1/objects/7913274c2f73bafcf888f09ff60990b100214ede/LINEAR_RGB.pf"}}, "executable": false, "type": "file"}, "lib/cmm/PYCC.pf": {"downloads": {"lzma": {"sha1": "dbb2197ecff3fcdd142e9006490c8cb5c8d19af8", "size": 171521, "url": "https://launcher.mojang.com/v1/objects/dbb2197ecff3fcdd142e9006490c8cb5c8d19af8/PYCC.pf"}, "raw": {"sha1": "4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144", "size": 274474, "url": "https://launcher.mojang.com/v1/objects/4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144/PYCC.pf"}}, "executable": false, "type": "file"}, "lib/cmm/sRGB.pf": {"downloads": {"raw": {"sha1": "9eaea0911d89d63e39e95f2e2116eaec7e0bb91e", "size": 3144, "url": "https://launcher.mojang.com/v1/objects/9eaea0911d89d63e39e95f2e2116eaec7e0bb91e/sRGB.pf"}}, "executable": false, "type": "file"}, "lib/content-types.properties": {"downloads": {"lzma": {"sha1": "43a23d9a6c637c128b14cfa3feced93cbcf85b1a", "size": 1617, "url": "https://launcher.mojang.com/v1/objects/43a23d9a6c637c128b14cfa3feced93cbcf85b1a/content-types.properties"}, "raw": {"sha1": "b21698017c4a2866b5fabe59681b7592e72c83b1", "size": 5916, "url": "https://launcher.mojang.com/v1/objects/b21698017c4a2866b5fabe59681b7592e72c83b1/content-types.properties"}}, "executable": false, "type": "file"}, "lib/currency.data": {"downloads": {"lzma": {"sha1": "451b3f166ea34ef2aefbb01606ea5adcc0d65b42", "size": 1184, "url": "https://launcher.mojang.com/v1/objects/451b3f166ea34ef2aefbb01606ea5adcc0d65b42/currency.data"}, "raw": {"sha1": "bf524381a7a9b9d5bbab48069c583d2936e367a1", "size": 4134, "url": "https://launcher.mojang.com/v1/objects/bf524381a7a9b9d5bbab48069c583d2936e367a1/currency.data"}}, "executable": false, "type": "file"}, "lib/deploy": {"type": "directory"}, "lib/deploy.jar": {"downloads": {"raw": {"sha1": "fbe1de8fcd9a3d482c59414dce9311e4194766c9", "size": 2255881, "url": "https://launcher.mojang.com/v1/objects/fbe1de8fcd9a3d482c59414dce9311e4194766c9/deploy.jar"}}, "executable": false, "type": "file"}, "lib/deploy/MixedCodeMainDialog.ui": {"downloads": {"lzma": {"sha1": "7d812964343d1e978442f5c847c709784fc18fc0", "size": 683, "url": "https://launcher.mojang.com/v1/objects/7d812964343d1e978442f5c847c709784fc18fc0/MixedCodeMainDialog.ui"}, "raw": {"sha1": "c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79", "size": 4507, "url": "https://launcher.mojang.com/v1/objects/c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79/MixedCodeMainDialog.ui"}}, "executable": false, "type": "file"}, "lib/deploy/MixedCodeMainDialogJs.ui": {"downloads": {"lzma": {"sha1": "54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2", "size": 792, "url": "https://launcher.mojang.com/v1/objects/54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2/MixedCodeMainDialogJs.ui"}, "raw": {"sha1": "ad6337fb6d46750e14c12b439a5856f4b6864d0d", "size": 6110, "url": "https://launcher.mojang.com/v1/objects/ad6337fb6d46750e14c12b439a5856f4b6864d0d/MixedCodeMainDialogJs.ui"}}, "executable": false, "type": "file"}, "lib/deploy/cautionshield.icns": {"downloads": {"lzma": {"sha1": "7cea751dc168605054ec38ce8bfa71812be405c1", "size": 2333, "url": "https://launcher.mojang.com/v1/objects/7cea751dc168605054ec38ce8bfa71812be405c1/cautionshield.icns"}, "raw": {"sha1": "1de7ed5d5fc75aa1bcede088c655bee3bde64c38", "size": 3588, "url": "https://launcher.mojang.com/v1/objects/1de7ed5d5fc75aa1bcede088c655bee3bde64c38/cautionshield.icns"}}, "executable": false, "type": "file"}, "lib/deploy/ffjcext.zip": {"downloads": {"lzma": {"sha1": "80bcb9b3794f69d87dba93e90230f288e651e798", "size": 1809, "url": "https://launcher.mojang.com/v1/objects/80bcb9b3794f69d87dba93e90230f288e651e798/ffjcext.zip"}, "raw": {"sha1": "76d051ca7d3666ff25ea8eb9957a05574a45287f", "size": 13454, "url": "https://launcher.mojang.com/v1/objects/76d051ca7d3666ff25ea8eb9957a05574a45287f/ffjcext.zip"}}, "executable": false, "type": "file"}, "lib/deploy/java-icon.ico": {"downloads": {"lzma": {"sha1": "2a24f0207d7ab5976a8b0d92b4b381d49e895c9d", "size": 8468, "url": "https://launcher.mojang.com/v1/objects/2a24f0207d7ab5976a8b0d92b4b381d49e895c9d/java-icon.ico"}, "raw": {"sha1": "2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914", "size": 29926, "url": "https://launcher.mojang.com/v1/objects/2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914/java-icon.ico"}}, "executable": false, "type": "file"}, "lib/deploy/messages.properties": {"downloads": {"lzma": {"sha1": "c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4", "size": 1225, "url": "https://launcher.mojang.com/v1/objects/c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4/messages.properties"}, "raw": {"sha1": "dc52841c708e3c1eb2a044088a43396d1291bb5e", "size": 2860, "url": "https://launcher.mojang.com/v1/objects/dc52841c708e3c1eb2a044088a43396d1291bb5e/messages.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_de.properties": {"downloads": {"lzma": {"sha1": "42b42e6e1d2cb2d781f2226bde612ce519b29bc8", "size": 1394, "url": "https://launcher.mojang.com/v1/objects/42b42e6e1d2cb2d781f2226bde612ce519b29bc8/messages_de.properties"}, "raw": {"sha1": "d989fe1b8f7904888d5102294ebefd28d932ecdb", "size": 3306, "url": "https://launcher.mojang.com/v1/objects/d989fe1b8f7904888d5102294ebefd28d932ecdb/messages_de.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_es.properties": {"downloads": {"lzma": {"sha1": "c4a653e9802ca982e892b45d88c1e259c09e8c8e", "size": 1404, "url": "https://launcher.mojang.com/v1/objects/c4a653e9802ca982e892b45d88c1e259c09e8c8e/messages_es.properties"}, "raw": {"sha1": "1b0334b79db481c3a59be6915d5118d760c97baa", "size": 3600, "url": "https://launcher.mojang.com/v1/objects/1b0334b79db481c3a59be6915d5118d760c97baa/messages_es.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_fr.properties": {"downloads": {"lzma": {"sha1": "2d8dee07e3f5aab7318a22e169810b216ac44f97", "size": 1401, "url": "https://launcher.mojang.com/v1/objects/2d8dee07e3f5aab7318a22e169810b216ac44f97/messages_fr.properties"}, "raw": {"sha1": "69bd2d03c2064f8679de5b4e430ea61b567c69c5", "size": 3409, "url": "https://launcher.mojang.com/v1/objects/69bd2d03c2064f8679de5b4e430ea61b567c69c5/messages_fr.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_it.properties": {"downloads": {"lzma": {"sha1": "7c28cdd8d9e34355ba0fc03004c4f64749cae57e", "size": 1375, "url": "https://launcher.mojang.com/v1/objects/7c28cdd8d9e34355ba0fc03004c4f64749cae57e/messages_it.properties"}, "raw": {"sha1": "dbe49949308f28540a42ae6cd2ad58afbf615592", "size": 3223, "url": "https://launcher.mojang.com/v1/objects/dbe49949308f28540a42ae6cd2ad58afbf615592/messages_it.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_ja.properties": {"downloads": {"lzma": {"sha1": "9a6a4c086e48b9e615b72b6bbebb3c724d178ff4", "size": 1680, "url": "https://launcher.mojang.com/v1/objects/9a6a4c086e48b9e615b72b6bbebb3c724d178ff4/messages_ja.properties"}, "raw": {"sha1": "751170a7cdefcb1226604ac3f8196e06a04fd7ac", "size": 6349, "url": "https://launcher.mojang.com/v1/objects/751170a7cdefcb1226604ac3f8196e06a04fd7ac/messages_ja.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_ko.properties": {"downloads": {"lzma": {"sha1": "0c57c2ebfa0830f816657a384898487fc492efac", "size": 1645, "url": "https://launcher.mojang.com/v1/objects/0c57c2ebfa0830f816657a384898487fc492efac/messages_ko.properties"}, "raw": {"sha1": "bf9e055b5ab138ad6d49769e2b7630b7938848d6", "size": 5712, "url": "https://launcher.mojang.com/v1/objects/bf9e055b5ab138ad6d49769e2b7630b7938848d6/messages_ko.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_pt_BR.properties": {"downloads": {"lzma": {"sha1": "f8364dba0aa0a7e445a1a8d0e7ad66b996f70063", "size": 1388, "url": "https://launcher.mojang.com/v1/objects/f8364dba0aa0a7e445a1a8d0e7ad66b996f70063/messages_pt_BR.properties"}, "raw": {"sha1": "24e4951743521ab9a11381c77bd0cdb1ed30f5b5", "size": 3285, "url": "https://launcher.mojang.com/v1/objects/24e4951743521ab9a11381c77bd0cdb1ed30f5b5/messages_pt_BR.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_sv.properties": {"downloads": {"lzma": {"sha1": "65e5897d552258141aacf02f087c7c9c33ad0727", "size": 1355, "url": "https://launcher.mojang.com/v1/objects/65e5897d552258141aacf02f087c7c9c33ad0727/messages_sv.properties"}, "raw": {"sha1": "bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb", "size": 3384, "url": "https://launcher.mojang.com/v1/objects/bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb/messages_sv.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_CN.properties": {"downloads": {"lzma": {"sha1": "de7d39a6e6748e9f47e842c9da90f515921c222c", "size": 1506, "url": "https://launcher.mojang.com/v1/objects/de7d39a6e6748e9f47e842c9da90f515921c222c/messages_zh_CN.properties"}, "raw": {"sha1": "1c2b96673dddd3596890ef4fc22017d484a1f652", "size": 4072, "url": "https://launcher.mojang.com/v1/objects/1c2b96673dddd3596890ef4fc22017d484a1f652/messages_zh_CN.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_HK.properties": {"downloads": {"lzma": {"sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties"}, "raw": {"sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_TW.properties": {"downloads": {"lzma": {"sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties"}, "raw": {"sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties"}}, "executable": false, "type": "file"}, "lib/deploy/mixcode_s.png": {"downloads": {"raw": {"sha1": "4604e9f265eec97bccd0151c3a81afa9e69499e5", "size": 3115, "url": "https://launcher.mojang.com/v1/objects/4604e9f265eec97bccd0151c3a81afa9e69499e5/mixcode_s.png"}}, "executable": false, "type": "file"}, "lib/deploy/splash.gif": {"downloads": {"raw": {"sha1": "20e7aec75f6d036d504277542e507eb7dc24aae8", "size": 8590, "url": "https://launcher.mojang.com/v1/objects/20e7aec75f6d036d504277542e507eb7dc24aae8/splash.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash@2x.gif": {"downloads": {"raw": {"sha1": "0ae4a5bda2a6d628fac51462390b503c99509fdc", "size": 15276, "url": "https://launcher.mojang.com/v1/objects/0ae4a5bda2a6d628fac51462390b503c99509fdc/splash2x.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash_11-lic.gif": {"downloads": {"raw": {"sha1": "8def364e07f40142822df84b5bb4f50846cb5e4e", "size": 7805, "url": "https://launcher.mojang.com/v1/objects/8def364e07f40142822df84b5bb4f50846cb5e4e/splash_11-lic.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash_11@2x-lic.gif": {"downloads": {"raw": {"sha1": "d2bff9bbf7920ca743b81a0ee23b0719b4d057ca", "size": 12250, "url": "https://launcher.mojang.com/v1/objects/d2bff9bbf7920ca743b81a0ee23b0719b4d057ca/splash_11%402x-lic.gif"}}, "executable": false, "type": "file"}, "lib/desktop": {"type": "directory"}, "lib/desktop/applications": {"type": "directory"}, "lib/desktop/applications/sun-java.desktop": {"downloads": {"lzma": {"sha1": "109d1cdf165f38da92da70b403ca940192a7a9a8", "size": 536, "url": "https://launcher.mojang.com/v1/objects/109d1cdf165f38da92da70b403ca940192a7a9a8/sun-java.desktop"}, "raw": {"sha1": "d346dfe90505603ce5aff5a3c6c2e4a23d5bd990", "size": 777, "url": "https://launcher.mojang.com/v1/objects/d346dfe90505603ce5aff5a3c6c2e4a23d5bd990/sun-java.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/applications/sun-javaws.desktop": {"downloads": {"lzma": {"sha1": "5e1815e7f83515881e6998584dc6bb02c5bef09a", "size": 451, "url": "https://launcher.mojang.com/v1/objects/5e1815e7f83515881e6998584dc6bb02c5bef09a/sun-javaws.desktop"}, "raw": {"sha1": "50ce8e519b836e0f53d58ce1a359d98b6cafdda6", "size": 619, "url": "https://launcher.mojang.com/v1/objects/50ce8e519b836e0f53d58ce1a359d98b6cafdda6/sun-javaws.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/applications/sun_java.desktop": {"downloads": {"lzma": {"sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop"}, "raw": {"sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/icons": {"type": "directory"}, "lib/desktop/icons/HighContrast": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/apps": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/apps": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/apps": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/apps": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/apps": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/apps": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/apps": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/apps": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/mime": {"type": "directory"}, "lib/desktop/mime/packages": {"type": "directory"}, "lib/desktop/mime/packages/x-java-archive.xml": {"downloads": {"lzma": {"sha1": "841230729f0a59de2a1071d155d96358232b2ba1", "size": 591, "url": "https://launcher.mojang.com/v1/objects/841230729f0a59de2a1071d155d96358232b2ba1/x-java-archive.xml"}, "raw": {"sha1": "b6297fd36efa799312961f95ebf0c85c920d5037", "size": 1822, "url": "https://launcher.mojang.com/v1/objects/b6297fd36efa799312961f95ebf0c85c920d5037/x-java-archive.xml"}}, "executable": false, "type": "file"}, "lib/desktop/mime/packages/x-java-jnlp-file.xml": {"downloads": {"lzma": {"sha1": "abf9acbe7c18027c4f036c4e42bb2cf1115525fa", "size": 302, "url": "https://launcher.mojang.com/v1/objects/abf9acbe7c18027c4f036c4e42bb2cf1115525fa/x-java-jnlp-file.xml"}, "raw": {"sha1": "72f03da83ddb76c9105f619fcfa4dbdad70e6b30", "size": 412, "url": "https://launcher.mojang.com/v1/objects/72f03da83ddb76c9105f619fcfa4dbdad70e6b30/x-java-jnlp-file.xml"}}, "executable": false, "type": "file"}, "lib/ext": {"type": "directory"}, "lib/ext/cldrdata.jar": {"downloads": {"raw": {"sha1": "6cacc961942d3f02a88907aa8f2eaae8e20c95a0", "size": 3860502, "url": "https://launcher.mojang.com/v1/objects/6cacc961942d3f02a88907aa8f2eaae8e20c95a0/cldrdata.jar"}}, "executable": false, "type": "file"}, "lib/ext/dnsns.jar": {"downloads": {"raw": {"sha1": "93bebdd7514e53ae31d60c5daba673878c8822ec", "size": 8286, "url": "https://launcher.mojang.com/v1/objects/93bebdd7514e53ae31d60c5daba673878c8822ec/dnsns.jar"}}, "executable": false, "type": "file"}, "lib/ext/jaccess.jar": {"downloads": {"raw": {"sha1": "2f54879df7c29ec67c40d40cfc95c0d4a968bea1", "size": 44516, "url": "https://launcher.mojang.com/v1/objects/2f54879df7c29ec67c40d40cfc95c0d4a968bea1/jaccess.jar"}}, "executable": false, "type": "file"}, "lib/ext/jfxrt.jar": {"downloads": {"lzma": {"sha1": "a6c5b6a782ba360ada6651f5322dcab88c75711c", "size": 3374270, "url": "https://launcher.mojang.com/v1/objects/a6c5b6a782ba360ada6651f5322dcab88c75711c/jfxrt.jar"}, "raw": {"sha1": "1ad7a876f045399c23ee4ab7dee380a04ca2ac08", "size": 18508094, "url": "https://launcher.mojang.com/v1/objects/1ad7a876f045399c23ee4ab7dee380a04ca2ac08/jfxrt.jar"}}, "executable": false, "type": "file"}, "lib/ext/localedata.jar": {"downloads": {"raw": {"sha1": "0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70", "size": 1178926, "url": "https://launcher.mojang.com/v1/objects/0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70/localedata.jar"}}, "executable": false, "type": "file"}, "lib/ext/meta-index": {"downloads": {"lzma": {"sha1": "1359457529f42bacf495afcb68149ae036442dd9", "size": 594, "url": "https://launcher.mojang.com/v1/objects/1359457529f42bacf495afcb68149ae036442dd9/meta-index"}, "raw": {"sha1": "334649c6e2d5d7248211f30855e97cfcb4558851", "size": 1269, "url": "https://launcher.mojang.com/v1/objects/334649c6e2d5d7248211f30855e97cfcb4558851/meta-index"}}, "executable": false, "type": "file"}, "lib/ext/nashorn.jar": {"downloads": {"raw": {"sha1": "dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5", "size": 2023869, "url": "https://launcher.mojang.com/v1/objects/dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5/nashorn.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunec.jar": {"downloads": {"raw": {"sha1": "bf1c817820341a246f7130fe046e8310b03d04f6", "size": 41672, "url": "https://launcher.mojang.com/v1/objects/bf1c817820341a246f7130fe046e8310b03d04f6/sunec.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunjce_provider.jar": {"downloads": {"raw": {"sha1": "bb3494e4b5cb3c3e60da767207731f18b267cb34", "size": 279326, "url": "https://launcher.mojang.com/v1/objects/bb3494e4b5cb3c3e60da767207731f18b267cb34/sunjce_provider.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunpkcs11.jar": {"downloads": {"raw": {"sha1": "5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4", "size": 250218, "url": "https://launcher.mojang.com/v1/objects/5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4/sunpkcs11.jar"}}, "executable": false, "type": "file"}, "lib/ext/zipfs.jar": {"downloads": {"raw": {"sha1": "37b338f0e8e60d6396f51275130e8110816d7b30", "size": 68964, "url": "https://launcher.mojang.com/v1/objects/37b338f0e8e60d6396f51275130e8110816d7b30/zipfs.jar"}}, "executable": false, "type": "file"}, "lib/flavormap.properties": {"downloads": {"lzma": {"sha1": "2d5ef19ee77ccfc95c9413eea155cde59a48fadd", "size": 1541, "url": "https://launcher.mojang.com/v1/objects/2d5ef19ee77ccfc95c9413eea155cde59a48fadd/flavormap.properties"}, "raw": {"sha1": "4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2", "size": 3901, "url": "https://launcher.mojang.com/v1/objects/4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2/flavormap.properties"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.5.bfc": {"downloads": {"lzma": {"sha1": "5197f6e387f16458b7408134e38b06f20f625e4c", "size": 795, "url": "https://launcher.mojang.com/v1/objects/5197f6e387f16458b7408134e38b06f20f625e4c/fontconfig.RedHat.5.bfc"}, "raw": {"sha1": "fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2", "size": 4532, "url": "https://launcher.mojang.com/v1/objects/fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2/fontconfig.RedHat.5.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.5.properties.src": {"downloads": {"lzma": {"sha1": "3897ae198e96e5a687c9c9b218ff5df60c868e0d", "size": 1089, "url": "https://launcher.mojang.com/v1/objects/3897ae198e96e5a687c9c9b218ff5df60c868e0d/fontconfig.RedHat.5.properties.src"}, "raw": {"sha1": "c67d1a06cb37b66e69560c9f5e4be7cf08af0493", "size": 8841, "url": "https://launcher.mojang.com/v1/objects/c67d1a06cb37b66e69560c9f5e4be7cf08af0493/fontconfig.RedHat.5.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.6.bfc": {"downloads": {"lzma": {"sha1": "ef2f5d1f8d620be9927db45d3a28bd75777245cb", "size": 818, "url": "https://launcher.mojang.com/v1/objects/ef2f5d1f8d620be9927db45d3a28bd75777245cb/fontconfig.RedHat.6.bfc"}, "raw": {"sha1": "9ba3b3e2c621c31d0ef1b2053c80f77419a19965", "size": 4250, "url": "https://launcher.mojang.com/v1/objects/9ba3b3e2c621c31d0ef1b2053c80f77419a19965/fontconfig.RedHat.6.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.6.properties.src": {"downloads": {"lzma": {"sha1": "74f4148f9d7ec3d67bbd724834d478a72cfdb0db", "size": 1111, "url": "https://launcher.mojang.com/v1/objects/74f4148f9d7ec3d67bbd724834d478a72cfdb0db/fontconfig.RedHat.6.properties.src"}, "raw": {"sha1": "768e58d4d314621c38daf9fde6d67119f370acd9", "size": 8735, "url": "https://launcher.mojang.com/v1/objects/768e58d4d314621c38daf9fde6d67119f370acd9/fontconfig.RedHat.6.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.10.bfc": {"downloads": {"lzma": {"sha1": "d8fe9b1d8d02368dcd452de93024c6f60670eb87", "size": 1083, "url": "https://launcher.mojang.com/v1/objects/d8fe9b1d8d02368dcd452de93024c6f60670eb87/fontconfig.SuSE.10.bfc"}, "raw": {"sha1": "ffd0dfbd1553e15b11649a73a0b3f48318138e0d", "size": 6702, "url": "https://launcher.mojang.com/v1/objects/ffd0dfbd1553e15b11649a73a0b3f48318138e0d/fontconfig.SuSE.10.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.10.properties.src": {"downloads": {"lzma": {"sha1": "2c382bd741a9e23114be3da82dee8290ebfca8a9", "size": 1555, "url": "https://launcher.mojang.com/v1/objects/2c382bd741a9e23114be3da82dee8290ebfca8a9/fontconfig.SuSE.10.properties.src"}, "raw": {"sha1": "a38dbdbbc514567b8281e1aea726865f37e97894", "size": 16772, "url": "https://launcher.mojang.com/v1/objects/a38dbdbbc514567b8281e1aea726865f37e97894/fontconfig.SuSE.10.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.11.bfc": {"downloads": {"lzma": {"sha1": "2b78cbf11289c9858951fea7180696ba3b7176d6", "size": 1092, "url": "https://launcher.mojang.com/v1/objects/2b78cbf11289c9858951fea7180696ba3b7176d6/fontconfig.SuSE.11.bfc"}, "raw": {"sha1": "a4d8500fcb47f6327460a95851b1368660da8302", "size": 7032, "url": "https://launcher.mojang.com/v1/objects/a4d8500fcb47f6327460a95851b1368660da8302/fontconfig.SuSE.11.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.11.properties.src": {"downloads": {"lzma": {"sha1": "5c1635803906e2c59d36492dec724dd7ae49a5ab", "size": 1589, "url": "https://launcher.mojang.com/v1/objects/5c1635803906e2c59d36492dec724dd7ae49a5ab/fontconfig.SuSE.11.properties.src"}, "raw": {"sha1": "c4b69589e41a7279a71866a9134213be19cdf88d", "size": 16781, "url": "https://launcher.mojang.com/v1/objects/c4b69589e41a7279a71866a9134213be19cdf88d/fontconfig.SuSE.11.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.Turbo.bfc": {"downloads": {"lzma": {"sha1": "1c771325d9ee4af209a3db92294451d58962c7a4", "size": 822, "url": "https://launcher.mojang.com/v1/objects/1c771325d9ee4af209a3db92294451d58962c7a4/fontconfig.Turbo.bfc"}, "raw": {"sha1": "f24368deeb85cc9d0781083bc56e773518d72219", "size": 4668, "url": "https://launcher.mojang.com/v1/objects/f24368deeb85cc9d0781083bc56e773518d72219/fontconfig.Turbo.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.Turbo.properties.src": {"downloads": {"lzma": {"sha1": "7748ffa17e2c8a34754138efa963ba39bd1cbbb3", "size": 1113, "url": "https://launcher.mojang.com/v1/objects/7748ffa17e2c8a34754138efa963ba39bd1cbbb3/fontconfig.Turbo.properties.src"}, "raw": {"sha1": "2bb7258bed7ccd4f117e4e5f892c9b13424b0c82", "size": 9192, "url": "https://launcher.mojang.com/v1/objects/2bb7258bed7ccd4f117e4e5f892c9b13424b0c82/fontconfig.Turbo.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.bfc": {"downloads": {"lzma": {"sha1": "be6d49ee8c64f458c4f0e64254963fec48d25150", "size": 286, "url": "https://launcher.mojang.com/v1/objects/be6d49ee8c64f458c4f0e64254963fec48d25150/fontconfig.bfc"}, "raw": {"sha1": "de39b0e19637f58d92a0188122514aa7247ebb5b", "size": 1678, "url": "https://launcher.mojang.com/v1/objects/de39b0e19637f58d92a0188122514aa7247ebb5b/fontconfig.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.properties.src": {"downloads": {"lzma": {"sha1": "9498d5e00e5401200667687e826e28c60fa60ba4", "size": 417, "url": "https://launcher.mojang.com/v1/objects/9498d5e00e5401200667687e826e28c60fa60ba4/fontconfig.properties.src"}, "raw": {"sha1": "3617ff1424fd204415242565541facf862b16eb4", "size": 1938, "url": "https://launcher.mojang.com/v1/objects/3617ff1424fd204415242565541facf862b16eb4/fontconfig.properties.src"}}, "executable": false, "type": "file"}, "lib/fonts": {"type": "directory"}, "lib/fonts/LucidaBrightDemiBold.ttf": {"downloads": {"lzma": {"sha1": "4f748750831a7719440dff5457f4d207d0f24d21", "size": 42347, "url": "https://launcher.mojang.com/v1/objects/4f748750831a7719440dff5457f4d207d0f24d21/LucidaBrightDemiBold.ttf"}, "raw": {"sha1": "b5c97f985639e19a3b712193ee48b55dda581fd1", "size": 75144, "url": "https://launcher.mojang.com/v1/objects/b5c97f985639e19a3b712193ee48b55dda581fd1/LucidaBrightDemiBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightDemiItalic.ttf": {"downloads": {"lzma": {"sha1": "f82e9a688553c100ecb98412b985807ed56dff5d", "size": 43119, "url": "https://launcher.mojang.com/v1/objects/f82e9a688553c100ecb98412b985807ed56dff5d/LucidaBrightDemiItalic.ttf"}, "raw": {"sha1": "1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7", "size": 75124, "url": "https://launcher.mojang.com/v1/objects/1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7/LucidaBrightDemiItalic.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightItalic.ttf": {"downloads": {"lzma": {"sha1": "6d630df719271319c3d53f90a3d425118b908266", "size": 46206, "url": "https://launcher.mojang.com/v1/objects/6d630df719271319c3d53f90a3d425118b908266/LucidaBrightItalic.ttf"}, "raw": {"sha1": "aa5c037865c563726ecd63d61ca26443589be425", "size": 80856, "url": "https://launcher.mojang.com/v1/objects/aa5c037865c563726ecd63d61ca26443589be425/LucidaBrightItalic.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightRegular.ttf": {"downloads": {"lzma": {"sha1": "4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe", "size": 181085, "url": "https://launcher.mojang.com/v1/objects/4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe/LucidaBrightRegular.ttf"}, "raw": {"sha1": "5d7ed564791c900a8786936930ba99385653139c", "size": 344908, "url": "https://launcher.mojang.com/v1/objects/5d7ed564791c900a8786936930ba99385653139c/LucidaBrightRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaSansDemiBold.ttf": {"downloads": {"lzma": {"sha1": "079b16dc3c4918ab1f4f760b6dc5e6586c219042", "size": 173229, "url": "https://launcher.mojang.com/v1/objects/079b16dc3c4918ab1f4f760b6dc5e6586c219042/LucidaSansDemiBold.ttf"}, "raw": {"sha1": "92b79fefc35e96190250c602a8fed85276b32a95", "size": 317896, "url": "https://launcher.mojang.com/v1/objects/92b79fefc35e96190250c602a8fed85276b32a95/LucidaSansDemiBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaSansRegular.ttf": {"downloads": {"lzma": {"sha1": "64a65d7b94d7153d20957ef6d06bebb4dd0f48e4", "size": 326062, "url": "https://launcher.mojang.com/v1/objects/64a65d7b94d7153d20957ef6d06bebb4dd0f48e4/LucidaSansRegular.ttf"}, "raw": {"sha1": "39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add", "size": 698236, "url": "https://launcher.mojang.com/v1/objects/39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add/LucidaSansRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaTypewriterBold.ttf": {"downloads": {"lzma": {"sha1": "cdb017f7c34bea0802bc5ea5583aef721ed99c49", "size": 130412, "url": "https://launcher.mojang.com/v1/objects/cdb017f7c34bea0802bc5ea5583aef721ed99c49/LucidaTypewriterBold.ttf"}, "raw": {"sha1": "a5da2eb49448f461470387c939f0e69119310e0b", "size": 234068, "url": "https://launcher.mojang.com/v1/objects/a5da2eb49448f461470387c939f0e69119310e0b/LucidaTypewriterBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaTypewriterRegular.ttf": {"downloads": {"lzma": {"sha1": "aeda4a09a53783b0dc97de8e20071bea874cbfe5", "size": 135184, "url": "https://launcher.mojang.com/v1/objects/aeda4a09a53783b0dc97de8e20071bea874cbfe5/LucidaTypewriterRegular.ttf"}, "raw": {"sha1": "c144dcafe4faf2e79cfd74d8134a631f30234db1", "size": 242700, "url": "https://launcher.mojang.com/v1/objects/c144dcafe4faf2e79cfd74d8134a631f30234db1/LucidaTypewriterRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/fonts.dir": {"downloads": {"lzma": {"sha1": "68f2dd93b215ec8b8d9409d2b9c825632c6b907d", "size": 273, "url": "https://launcher.mojang.com/v1/objects/68f2dd93b215ec8b8d9409d2b9c825632c6b907d/fonts.dir"}, "raw": {"sha1": "97f40cca185c954adf5cc582345a7cb8e4c50578", "size": 4041, "url": "https://launcher.mojang.com/v1/objects/97f40cca185c954adf5cc582345a7cb8e4c50578/fonts.dir"}}, "executable": false, "type": "file"}, "lib/hijrah-config-umalqura.properties": {"downloads": {"lzma": {"sha1": "02e8d296e3b18a450f1ed1547cbf2b7275664c9a", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/02e8d296e3b18a450f1ed1547cbf2b7275664c9a/hijrah-config-umalqura.properties"}, "raw": {"sha1": "84aa425100740722e91f4725caf849e7863d12ba", "size": 13962, "url": "https://launcher.mojang.com/v1/objects/84aa425100740722e91f4725caf849e7863d12ba/hijrah-config-umalqura.properties"}}, "executable": false, "type": "file"}, "lib/images": {"type": "directory"}, "lib/images/cursors": {"type": "directory"}, "lib/images/cursors/cursors.properties": {"downloads": {"lzma": {"sha1": "612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f", "size": 385, "url": "https://launcher.mojang.com/v1/objects/612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f/cursors.properties"}, "raw": {"sha1": "f2b9a22ddd0a77869497a64f28f07e89a7d41f48", "size": 1274, "url": "https://launcher.mojang.com/v1/objects/f2b9a22ddd0a77869497a64f28f07e89a7d41f48/cursors.properties"}}, "executable": false, "type": "file"}, "lib/images/cursors/invalid32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_CopyDrop32x32.gif": {"downloads": {"raw": {"sha1": "eb7620fae702172aa663a19d170a0b929d3b11d1", "size": 158, "url": "https://launcher.mojang.com/v1/objects/eb7620fae702172aa663a19d170a0b929d3b11d1/motif_CopyDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_CopyNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_LinkDrop32x32.gif": {"downloads": {"raw": {"sha1": "9699137f990c240e714481563181069c8f6c17bb", "size": 162, "url": "https://launcher.mojang.com/v1/objects/9699137f990c240e714481563181069c8f6c17bb/motif_LinkDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_LinkNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_MoveDrop32x32.gif": {"downloads": {"raw": {"sha1": "03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b", "size": 141, "url": "https://launcher.mojang.com/v1/objects/03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b/motif_MoveDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_MoveNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/icons": {"type": "directory"}, "lib/images/icons/sun-java.png": {"downloads": {"raw": {"sha1": "d101b693aa054f51097eebdfeed8b8a6ca7b55b8", "size": 4707, "url": "https://launcher.mojang.com/v1/objects/d101b693aa054f51097eebdfeed8b8a6ca7b55b8/sun-java.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_HighContrast.png": {"downloads": {"raw": {"sha1": "a6b1e418d6b5d03719b96f61f0c5236a60970151", "size": 3729, "url": "https://launcher.mojang.com/v1/objects/a6b1e418d6b5d03719b96f61f0c5236a60970151/sun-java_HighContrast.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_HighContrastInverse.png": {"downloads": {"raw": {"sha1": "2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887", "size": 3777, "url": "https://launcher.mojang.com/v1/objects/2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887/sun-java_HighContrastInverse.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_LowContrast.png": {"downloads": {"raw": {"sha1": "7714cc4e894c3626c8da6fe742ed22b2829122d9", "size": 4012, "url": "https://launcher.mojang.com/v1/objects/7714cc4e894c3626c8da6fe742ed22b2829122d9/sun-java_LowContrast.png"}}, "executable": false, "type": "file"}, "lib/javafx.properties": {"downloads": {"raw": {"sha1": "49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d", "size": 56, "url": "https://launcher.mojang.com/v1/objects/49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d/javafx.properties"}}, "executable": false, "type": "file"}, "lib/javaws.jar": {"downloads": {"raw": {"sha1": "04fa5ae04ead65b91be5dee575497e49ffd49fe9", "size": 488118, "url": "https://launcher.mojang.com/v1/objects/04fa5ae04ead65b91be5dee575497e49ffd49fe9/javaws.jar"}}, "executable": false, "type": "file"}, "lib/jce.jar": {"downloads": {"raw": {"sha1": "5460adee09cc5fc8829c0acfc46c34670a7d70a0", "size": 115646, "url": "https://launcher.mojang.com/v1/objects/5460adee09cc5fc8829c0acfc46c34670a7d70a0/jce.jar"}}, "executable": false, "type": "file"}, "lib/jexec": {"downloads": {"lzma": {"sha1": "2d4323d4e060f8126d026ca6c03b8972aedd2fab", "size": 3311, "url": "https://launcher.mojang.com/v1/objects/2d4323d4e060f8126d026ca6c03b8972aedd2fab/jexec"}, "raw": {"sha1": "6aa01f1d8d103974164bcfaea03c04eeeefd7d41", "size": 13376, "url": "https://launcher.mojang.com/v1/objects/6aa01f1d8d103974164bcfaea03c04eeeefd7d41/jexec"}}, "executable": true, "type": "file"}, "lib/jfr": {"type": "directory"}, "lib/jfr.jar": {"downloads": {"lzma": {"sha1": "5b9d615c91c72f4fe356d9b4105946679452d1e1", "size": 137982, "url": "https://launcher.mojang.com/v1/objects/5b9d615c91c72f4fe356d9b4105946679452d1e1/jfr.jar"}, "raw": {"sha1": "0f3fd66a336703d935bdc22ad8082bc51d34e534", "size": 560713, "url": "https://launcher.mojang.com/v1/objects/0f3fd66a336703d935bdc22ad8082bc51d34e534/jfr.jar"}}, "executable": false, "type": "file"}, "lib/jfr/default.jfc": {"downloads": {"lzma": {"sha1": "373ddd878146dd8ce8991c2c5115a05a82859bdb", "size": 2207, "url": "https://launcher.mojang.com/v1/objects/373ddd878146dd8ce8991c2c5115a05a82859bdb/default.jfc"}, "raw": {"sha1": "1a64b68d0e7d43f8149faba94440be54f4f24527", "size": 20109, "url": "https://launcher.mojang.com/v1/objects/1a64b68d0e7d43f8149faba94440be54f4f24527/default.jfc"}}, "executable": false, "type": "file"}, "lib/jfr/profile.jfc": {"downloads": {"lzma": {"sha1": "3dcdc5feee3ccfb66bc8726b666944cd4bdadae3", "size": 2199, "url": "https://launcher.mojang.com/v1/objects/3dcdc5feee3ccfb66bc8726b666944cd4bdadae3/profile.jfc"}, "raw": {"sha1": "5d7d08a595f76322c51ae43ea966fbba6b69eebe", "size": 20065, "url": "https://launcher.mojang.com/v1/objects/5d7d08a595f76322c51ae43ea966fbba6b69eebe/profile.jfc"}}, "executable": false, "type": "file"}, "lib/jfxswt.jar": {"downloads": {"raw": {"sha1": "99d9a264c898d84c01e1c42565e7fe1a89dcd72d", "size": 33932, "url": "https://launcher.mojang.com/v1/objects/99d9a264c898d84c01e1c42565e7fe1a89dcd72d/jfxswt.jar"}}, "executable": false, "type": "file"}, "lib/jsse.jar": {"downloads": {"lzma": {"sha1": "94a17dfbc2e76cd12c33970a15341424f875a9ce", "size": 187549, "url": "https://launcher.mojang.com/v1/objects/94a17dfbc2e76cd12c33970a15341424f875a9ce/jsse.jar"}, "raw": {"sha1": "92c5c626e8a2d16f41272c0e404d4f992dd8310a", "size": 675599, "url": "https://launcher.mojang.com/v1/objects/92c5c626e8a2d16f41272c0e404d4f992dd8310a/jsse.jar"}}, "executable": false, "type": "file"}, "lib/jvm.hprof.txt": {"downloads": {"lzma": {"sha1": "eccdb240a815b2a83a502749339b27bb8669965b", "size": 1863, "url": "https://launcher.mojang.com/v1/objects/eccdb240a815b2a83a502749339b27bb8669965b/jvm.hprof.txt"}, "raw": {"sha1": "fbd61d52534cdd0c15df332114d469c65d001e33", "size": 4226, "url": "https://launcher.mojang.com/v1/objects/fbd61d52534cdd0c15df332114d469c65d001e33/jvm.hprof.txt"}}, "executable": false, "type": "file"}, "lib/locale": {"type": "directory"}, "lib/locale/de": {"type": "directory"}, "lib/locale/de/LC_MESSAGES": {"type": "directory"}, "lib/locale/de/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3061d922907cc557208109088fc6ab81d577ff6f", "size": 970, "url": "https://launcher.mojang.com/v1/objects/3061d922907cc557208109088fc6ab81d577ff6f/sunw_java_plugin.mo"}, "raw": {"sha1": "5b223a3d723ac1cfce63623fb109f2868d47d1b7", "size": 2483, "url": "https://launcher.mojang.com/v1/objects/5b223a3d723ac1cfce63623fb109f2868d47d1b7/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/es": {"type": "directory"}, "lib/locale/es/LC_MESSAGES": {"type": "directory"}, "lib/locale/es/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "24338049a89b323e17182b3a3006b50565d4fa0f", "size": 979, "url": "https://launcher.mojang.com/v1/objects/24338049a89b323e17182b3a3006b50565d4fa0f/sunw_java_plugin.mo"}, "raw": {"sha1": "6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1", "size": 2477, "url": "https://launcher.mojang.com/v1/objects/6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/fr": {"type": "directory"}, "lib/locale/fr/LC_MESSAGES": {"type": "directory"}, "lib/locale/fr/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "22796a48ef39f57d2d6fa70f41308e493d7f05c1", "size": 1033, "url": "https://launcher.mojang.com/v1/objects/22796a48ef39f57d2d6fa70f41308e493d7f05c1/sunw_java_plugin.mo"}, "raw": {"sha1": "d9d5b458db6e83fdf85c3526aeee3f57c4929840", "size": 2746, "url": "https://launcher.mojang.com/v1/objects/d9d5b458db6e83fdf85c3526aeee3f57c4929840/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/it": {"type": "directory"}, "lib/locale/it/LC_MESSAGES": {"type": "directory"}, "lib/locale/it/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "59a4cae38bfb8927745674d0efc2f284bc277987", "size": 958, "url": "https://launcher.mojang.com/v1/objects/59a4cae38bfb8927745674d0efc2f284bc277987/sunw_java_plugin.mo"}, "raw": {"sha1": "f6e72e3b2141ccc3dffab10ae14a754e494577ba", "size": 2434, "url": "https://launcher.mojang.com/v1/objects/f6e72e3b2141ccc3dffab10ae14a754e494577ba/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ja": {"type": "directory"}, "lib/locale/ja/LC_MESSAGES": {"type": "directory"}, "lib/locale/ja/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "7d6aeed563e1cefcf0224cf522048468088884a9", "size": 1036, "url": "https://launcher.mojang.com/v1/objects/7d6aeed563e1cefcf0224cf522048468088884a9/sunw_java_plugin.mo"}, "raw": {"sha1": "378881a8cb8dd2aebb43eacd0c68519be4f258b1", "size": 2415, "url": "https://launcher.mojang.com/v1/objects/378881a8cb8dd2aebb43eacd0c68519be4f258b1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ko": {"type": "directory"}, "lib/locale/ko.UTF-8": {"type": "directory"}, "lib/locale/ko.UTF-8/LC_MESSAGES": {"type": "directory"}, "lib/locale/ko.UTF-8/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "12ee3b21511e8497d95ea0ba9d6fe519227d0b16", "size": 1069, "url": "https://launcher.mojang.com/v1/objects/12ee3b21511e8497d95ea0ba9d6fe519227d0b16/sunw_java_plugin.mo"}, "raw": {"sha1": "cb19df01c59662dbe2f4050b1290d374b82fe1fa", "size": 2753, "url": "https://launcher.mojang.com/v1/objects/cb19df01c59662dbe2f4050b1290d374b82fe1fa/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ko/LC_MESSAGES": {"type": "directory"}, "lib/locale/ko/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "6e2e47c64c360517fd436bc79c823b5679a1efe6", "size": 996, "url": "https://launcher.mojang.com/v1/objects/6e2e47c64c360517fd436bc79c823b5679a1efe6/sunw_java_plugin.mo"}, "raw": {"sha1": "12c8a118d150c78f719314df6dec49a967af71e9", "size": 2399, "url": "https://launcher.mojang.com/v1/objects/12c8a118d150c78f719314df6dec49a967af71e9/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/pt_BR": {"type": "directory"}, "lib/locale/pt_BR/LC_MESSAGES": {"type": "directory"}, "lib/locale/pt_BR/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "bcaa7e7916493f071f1bf64bf58c6b038e3569c9", "size": 940, "url": "https://launcher.mojang.com/v1/objects/bcaa7e7916493f071f1bf64bf58c6b038e3569c9/sunw_java_plugin.mo"}, "raw": {"sha1": "a3bc0c43994c53c59bba94982cf95f6d36283dd0", "size": 2420, "url": "https://launcher.mojang.com/v1/objects/a3bc0c43994c53c59bba94982cf95f6d36283dd0/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/sv": {"type": "directory"}, "lib/locale/sv/LC_MESSAGES": {"type": "directory"}, "lib/locale/sv/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "76017835d6261fe2eedbcbe5eb08a7484c3080c5", "size": 946, "url": "https://launcher.mojang.com/v1/objects/76017835d6261fe2eedbcbe5eb08a7484c3080c5/sunw_java_plugin.mo"}, "raw": {"sha1": "09a47686edec4bbb34e82fbd08559f8bb6266544", "size": 2359, "url": "https://launcher.mojang.com/v1/objects/09a47686edec4bbb34e82fbd08559f8bb6266544/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh": {"type": "directory"}, "lib/locale/zh.GBK": {"type": "directory"}, "lib/locale/zh.GBK/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh.GBK/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo"}, "raw": {"sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo"}, "raw": {"sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_HK.BIG5HK": {"type": "directory"}, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo"}, "raw": {"sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_TW": {"type": "directory"}, "lib/locale/zh_TW.BIG5": {"type": "directory"}, "lib/locale/zh_TW.BIG5/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_TW.BIG5/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo"}, "raw": {"sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_TW/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_TW/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "c05e610e75182f0c4e77f3e7a4d9670ed62bf63c", "size": 897, "url": "https://launcher.mojang.com/v1/objects/c05e610e75182f0c4e77f3e7a4d9670ed62bf63c/sunw_java_plugin.mo"}, "raw": {"sha1": "f9b972dd059eae3cd337dfcef6a178e8ed8a7db6", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/f9b972dd059eae3cd337dfcef6a178e8ed8a7db6/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/logging.properties": {"downloads": {"lzma": {"sha1": "642202a58e5216d086ad37c0b5a633be802edc78", "size": 896, "url": "https://launcher.mojang.com/v1/objects/642202a58e5216d086ad37c0b5a633be802edc78/logging.properties"}, "raw": {"sha1": "89da8094484891f9ec1fa40c6c8b61f94c5869d0", "size": 2455, "url": "https://launcher.mojang.com/v1/objects/89da8094484891f9ec1fa40c6c8b61f94c5869d0/logging.properties"}}, "executable": false, "type": "file"}, "lib/management": {"type": "directory"}, "lib/management-agent.jar": {"downloads": {"lzma": {"sha1": "3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd", "size": 243, "url": "https://launcher.mojang.com/v1/objects/3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd/management-agent.jar"}, "raw": {"sha1": "9fbed36522aa3a80bac08a328942cbc5ef39ca8e", "size": 381, "url": "https://launcher.mojang.com/v1/objects/9fbed36522aa3a80bac08a328942cbc5ef39ca8e/management-agent.jar"}}, "executable": false, "type": "file"}, "lib/management/jmxremote.access": {"downloads": {"lzma": {"sha1": "69042ff1b14165db19c9d728614639dec16d6a31", "size": 1419, "url": "https://launcher.mojang.com/v1/objects/69042ff1b14165db19c9d728614639dec16d6a31/jmxremote.access"}, "raw": {"sha1": "21200eaad898ba4a2a8834a032efb6616fabb930", "size": 3998, "url": "https://launcher.mojang.com/v1/objects/21200eaad898ba4a2a8834a032efb6616fabb930/jmxremote.access"}}, "executable": false, "type": "file"}, "lib/management/jmxremote.password.template": {"downloads": {"lzma": {"sha1": "556c64b1e920766f8867be3964de6e49f5b81a60", "size": 1129, "url": "https://launcher.mojang.com/v1/objects/556c64b1e920766f8867be3964de6e49f5b81a60/jmxremote.password.template"}, "raw": {"sha1": "c1e0f01408bf20fbbb8b4810520c725f70050db5", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/c1e0f01408bf20fbbb8b4810520c725f70050db5/jmxremote.password.template"}}, "executable": false, "type": "file"}, "lib/management/management.properties": {"downloads": {"lzma": {"sha1": "3e52f9baa6394ca6956845424c607e5cde5d3c67", "size": 3176, "url": "https://launcher.mojang.com/v1/objects/3e52f9baa6394ca6956845424c607e5cde5d3c67/management.properties"}, "raw": {"sha1": "e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1", "size": 14630, "url": "https://launcher.mojang.com/v1/objects/e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1/management.properties"}}, "executable": false, "type": "file"}, "lib/management/snmp.acl.template": {"downloads": {"lzma": {"sha1": "9a4aa6396c3b488b0663bed5e5ecb762985669c9", "size": 1121, "url": "https://launcher.mojang.com/v1/objects/9a4aa6396c3b488b0663bed5e5ecb762985669c9/snmp.acl.template"}, "raw": {"sha1": "2e9f9ac287274532eb1f0d1afcefd7f3e97cc794", "size": 3376, "url": "https://launcher.mojang.com/v1/objects/2e9f9ac287274532eb1f0d1afcefd7f3e97cc794/snmp.acl.template"}}, "executable": false, "type": "file"}, "lib/meta-index": {"downloads": {"lzma": {"sha1": "1ac60b31362fda4725c665b591c5fbe384cbc8c1", "size": 788, "url": "https://launcher.mojang.com/v1/objects/1ac60b31362fda4725c665b591c5fbe384cbc8c1/meta-index"}, "raw": {"sha1": "bf204f09242203e713c31785158a0792f9edb600", "size": 2034, "url": "https://launcher.mojang.com/v1/objects/bf204f09242203e713c31785158a0792f9edb600/meta-index"}}, "executable": false, "type": "file"}, "lib/net.properties": {"downloads": {"lzma": {"sha1": "e9ec3981a0797bf55bb87b24d9eb651ce7e6916b", "size": 1830, "url": "https://launcher.mojang.com/v1/objects/e9ec3981a0797bf55bb87b24d9eb651ce7e6916b/net.properties"}, "raw": {"sha1": "fd9471742eb759f4478bb1de9a0dc0527265b6ea", "size": 5352, "url": "https://launcher.mojang.com/v1/objects/fd9471742eb759f4478bb1de9a0dc0527265b6ea/net.properties"}}, "executable": false, "type": "file"}, "lib/oblique-fonts": {"type": "directory"}, "lib/oblique-fonts/LucidaSansDemiOblique.ttf": {"downloads": {"lzma": {"sha1": "49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2", "size": 38580, "url": "https://launcher.mojang.com/v1/objects/49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2/LucidaSansDemiOblique.ttf"}, "raw": {"sha1": "53e4e12a675ac222469341c3dbc102464a1be4c7", "size": 91352, "url": "https://launcher.mojang.com/v1/objects/53e4e12a675ac222469341c3dbc102464a1be4c7/LucidaSansDemiOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaSansOblique.ttf": {"downloads": {"lzma": {"sha1": "553123c0edcd08035dede4ffd92b5b81c9a7538a", "size": 116575, "url": "https://launcher.mojang.com/v1/objects/553123c0edcd08035dede4ffd92b5b81c9a7538a/LucidaSansOblique.ttf"}, "raw": {"sha1": "95a195ad4fc520b3e395c85b747fc3024d118dd9", "size": 253724, "url": "https://launcher.mojang.com/v1/objects/95a195ad4fc520b3e395c85b747fc3024d118dd9/LucidaSansOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaTypewriterBoldOblique.ttf": {"downloads": {"lzma": {"sha1": "2475b08151556ad4d89bb1d2b6494c6bee9abd82", "size": 29954, "url": "https://launcher.mojang.com/v1/objects/2475b08151556ad4d89bb1d2b6494c6bee9abd82/LucidaTypewriterBoldOblique.ttf"}, "raw": {"sha1": "f331fc8b0cc494702bc46b690f2b8eed36469a02", "size": 63168, "url": "https://launcher.mojang.com/v1/objects/f331fc8b0cc494702bc46b690f2b8eed36469a02/LucidaTypewriterBoldOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaTypewriterOblique.ttf": {"downloads": {"lzma": {"sha1": "5b970bc3b7abb21dce1aa28ff7f03459d351e552", "size": 60133, "url": "https://launcher.mojang.com/v1/objects/5b970bc3b7abb21dce1aa28ff7f03459d351e552/LucidaTypewriterOblique.ttf"}, "raw": {"sha1": "f8ea00db73f8a89a27674d050edc37c2280930e1", "size": 137484, "url": "https://launcher.mojang.com/v1/objects/f8ea00db73f8a89a27674d050edc37c2280930e1/LucidaTypewriterOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/fonts.dir": {"downloads": {"lzma": {"sha1": "067528c789bd713c7c3f34e779aa6e2e8253dcf6", "size": 188, "url": "https://launcher.mojang.com/v1/objects/067528c789bd713c7c3f34e779aa6e2e8253dcf6/fonts.dir"}, "raw": {"sha1": "5aee54ffba9e33de56fd84ef64fa496b898585bb", "size": 2115, "url": "https://launcher.mojang.com/v1/objects/5aee54ffba9e33de56fd84ef64fa496b898585bb/fonts.dir"}}, "executable": false, "type": "file"}, "lib/plugin.jar": {"downloads": {"raw": {"sha1": "3f250842c79112bae5369e372025b166990820e8", "size": 950772, "url": "https://launcher.mojang.com/v1/objects/3f250842c79112bae5369e372025b166990820e8/plugin.jar"}}, "executable": false, "type": "file"}, "lib/psfont.properties.ja": {"downloads": {"lzma": {"sha1": "7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4", "size": 701, "url": "https://launcher.mojang.com/v1/objects/7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4/psfont.properties.ja"}, "raw": {"sha1": "56ed1c661eeede17b4fae8c9de7b5edbad387abc", "size": 2796, "url": "https://launcher.mojang.com/v1/objects/56ed1c661eeede17b4fae8c9de7b5edbad387abc/psfont.properties.ja"}}, "executable": false, "type": "file"}, "lib/psfontj2d.properties": {"downloads": {"lzma": {"sha1": "4252fa01af8739a3545e2b705e3383892e22ab40", "size": 2278, "url": "https://launcher.mojang.com/v1/objects/4252fa01af8739a3545e2b705e3383892e22ab40/psfontj2d.properties"}, "raw": {"sha1": "aa327a22a49967f4d74afeee6726f505f209692f", "size": 10393, "url": "https://launcher.mojang.com/v1/objects/aa327a22a49967f4d74afeee6726f505f209692f/psfontj2d.properties"}}, "executable": false, "type": "file"}, "lib/resources.jar": {"downloads": {"lzma": {"sha1": "1b0e08441750dc17efe4b527aa146da6cc14e8a6", "size": 579294, "url": "https://launcher.mojang.com/v1/objects/1b0e08441750dc17efe4b527aa146da6cc14e8a6/resources.jar"}, "raw": {"sha1": "daa021906e4648d4c37e798c11733dc2047f2da1", "size": 3505206, "url": "https://launcher.mojang.com/v1/objects/daa021906e4648d4c37e798c11733dc2047f2da1/resources.jar"}}, "executable": false, "type": "file"}, "lib/rt.jar": {"downloads": {"lzma": {"sha1": "fc4a8681aeda29c2a2a3fd11bad7729543283f3d", "size": 14378994, "url": "https://launcher.mojang.com/v1/objects/fc4a8681aeda29c2a2a3fd11bad7729543283f3d/rt.jar"}, "raw": {"sha1": "5396b0954a20f3210f1f4f1886ead30880d6ebfe", "size": 66334986, "url": "https://launcher.mojang.com/v1/objects/5396b0954a20f3210f1f4f1886ead30880d6ebfe/rt.jar"}}, "executable": false, "type": "file"}, "lib/security": {"type": "directory"}, "lib/security/blacklist": {"downloads": {"lzma": {"sha1": "8206fce6c1d91a39fdf78e8e79e953913994a1cd", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/8206fce6c1d91a39fdf78e8e79e953913994a1cd/blacklist"}, "raw": {"sha1": "d4ffb3857eab403955ce9d156e46d056061e6a5a", "size": 4054, "url": "https://launcher.mojang.com/v1/objects/d4ffb3857eab403955ce9d156e46d056061e6a5a/blacklist"}}, "executable": false, "type": "file"}, "lib/security/blacklisted.certs": {"downloads": {"lzma": {"sha1": "8311bead054caf6cfe678d4b7998de4caaabfa53", "size": 806, "url": "https://launcher.mojang.com/v1/objects/8311bead054caf6cfe678d4b7998de4caaabfa53/blacklisted.certs"}, "raw": {"sha1": "c5c005c29a80493f5c31cd7eb629ac1b9c752404", "size": 1273, "url": "https://launcher.mojang.com/v1/objects/c5c005c29a80493f5c31cd7eb629ac1b9c752404/blacklisted.certs"}}, "executable": false, "type": "file"}, "lib/security/cacerts": {"downloads": {"lzma": {"sha1": "654dd94809655d5b28385cbb5eba8d6ad9f2c1aa", "size": 67802, "url": "https://launcher.mojang.com/v1/objects/654dd94809655d5b28385cbb5eba8d6ad9f2c1aa/cacerts"}, "raw": {"sha1": "2917859c443c68e19f93abcd1315c3c2904cbef9", "size": 104430, "url": "https://launcher.mojang.com/v1/objects/2917859c443c68e19f93abcd1315c3c2904cbef9/cacerts"}}, "executable": false, "type": "file"}, "lib/security/java.policy": {"downloads": {"lzma": {"sha1": "b601c420d02ef3dbd8595453d08fdef91134e8b5", "size": 647, "url": "https://launcher.mojang.com/v1/objects/b601c420d02ef3dbd8595453d08fdef91134e8b5/java.policy"}, "raw": {"sha1": "c0112209a567b3b523cfed7041709f9440227968", "size": 2466, "url": "https://launcher.mojang.com/v1/objects/c0112209a567b3b523cfed7041709f9440227968/java.policy"}}, "executable": false, "type": "file"}, "lib/security/java.security": {"downloads": {"lzma": {"sha1": "531620e82ca0365ce8dc97096bb0ac5a7ace5952", "size": 10959, "url": "https://launcher.mojang.com/v1/objects/531620e82ca0365ce8dc97096bb0ac5a7ace5952/java.security"}, "raw": {"sha1": "5dcc17a168c53d0b366784e520bd4d55aa61ac18", "size": 41528, "url": "https://launcher.mojang.com/v1/objects/5dcc17a168c53d0b366784e520bd4d55aa61ac18/java.security"}}, "executable": false, "type": "file"}, "lib/security/javaws.policy": {"downloads": {"raw": {"sha1": "4384ca5e4d32f7dd86d8baddd1e690730d74e694", "size": 98, "url": "https://launcher.mojang.com/v1/objects/4384ca5e4d32f7dd86d8baddd1e690730d74e694/javaws.policy"}}, "executable": false, "type": "file"}, "lib/security/policy": {"type": "directory"}, "lib/security/policy/limited": {"type": "directory"}, "lib/security/policy/limited/US_export_policy.jar": {"downloads": {"raw": {"sha1": "7d69ea3b385bc067738520f1b5c549e1084be285", "size": 3026, "url": "https://launcher.mojang.com/v1/objects/7d69ea3b385bc067738520f1b5c549e1084be285/US_export_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/limited/local_policy.jar": {"downloads": {"raw": {"sha1": "238b8826e110f58acb2e1959773b0a577cd4d569", "size": 3527, "url": "https://launcher.mojang.com/v1/objects/238b8826e110f58acb2e1959773b0a577cd4d569/local_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/unlimited": {"type": "directory"}, "lib/security/policy/unlimited/US_export_policy.jar": {"downloads": {"raw": {"sha1": "f6fb2af1e87fc622cda194a7d6b5f5f069653ff1", "size": 3023, "url": "https://launcher.mojang.com/v1/objects/f6fb2af1e87fc622cda194a7d6b5f5f069653ff1/US_export_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/unlimited/local_policy.jar": {"downloads": {"raw": {"sha1": "517368ab2cbaf6b42ea0b963f98eeedd996e83e3", "size": 3035, "url": "https://launcher.mojang.com/v1/objects/517368ab2cbaf6b42ea0b963f98eeedd996e83e3/local_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/trusted.libraries": {"downloads": {"raw": {"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "size": 0, "url": "https://launcher.mojang.com/v1/objects/da39a3ee5e6b4b0d3255bfef95601890afd80709/trusted.libraries"}}, "executable": false, "type": "file"}, "lib/sound.properties": {"downloads": {"lzma": {"sha1": "3b5f7e4ec437d79048af35094290577f483b3fe1", "size": 473, "url": "https://launcher.mojang.com/v1/objects/3b5f7e4ec437d79048af35094290577f483b3fe1/sound.properties"}, "raw": {"sha1": "9afceb218059d981d0fa9f07aad3c5097cf41b0c", "size": 1210, "url": "https://launcher.mojang.com/v1/objects/9afceb218059d981d0fa9f07aad3c5097cf41b0c/sound.properties"}}, "executable": false, "type": "file"}, "lib/tzdb.dat": {"downloads": {"lzma": {"sha1": "39c69339965484afe89c14111baeeb862fdefd97", "size": 32547, "url": "https://launcher.mojang.com/v1/objects/39c69339965484afe89c14111baeeb862fdefd97/tzdb.dat"}, "raw": {"sha1": "b59c07e3619271a3b9861e999f4b138e971baf69", "size": 105734, "url": "https://launcher.mojang.com/v1/objects/b59c07e3619271a3b9861e999f4b138e971baf69/tzdb.dat"}}, "executable": false, "type": "file"}, "man": {"type": "directory"}, "man/ja": {"target": "ja_JP.UTF-8", "type": "link"}, "man/ja_JP.UTF-8": {"type": "directory"}, "man/ja_JP.UTF-8/man1": {"type": "directory"}, "man/ja_JP.UTF-8/man1/java.1": {"downloads": {"lzma": {"sha1": "f9da09710b6c6df23c256e324a0c4df00a0d6ded", "size": 25461, "url": "https://launcher.mojang.com/v1/objects/f9da09710b6c6df23c256e324a0c4df00a0d6ded/java.1"}, "raw": {"sha1": "b0b12a0bb66e6171771ca4b1dfca32fb759bcaec", "size": 148688, "url": "https://launcher.mojang.com/v1/objects/b0b12a0bb66e6171771ca4b1dfca32fb759bcaec/java.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/javaws.1": {"downloads": {"lzma": {"sha1": "6188fae453ca09ccb19be5c9f4d2059926b36267", "size": 2154, "url": "https://launcher.mojang.com/v1/objects/6188fae453ca09ccb19be5c9f4d2059926b36267/javaws.1"}, "raw": {"sha1": "8f39d928870268ace07bedfebd18db1e1d07fc37", "size": 6641, "url": "https://launcher.mojang.com/v1/objects/8f39d928870268ace07bedfebd18db1e1d07fc37/javaws.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/jjs.1": {"downloads": {"lzma": {"sha1": "6e42b989d28b185dc1aab50c0389834e649a37d4", "size": 3452, "url": "https://launcher.mojang.com/v1/objects/6e42b989d28b185dc1aab50c0389834e649a37d4/jjs.1"}, "raw": {"sha1": "e023322a2013912315a2bd1034e6f829a27c76e0", "size": 11365, "url": "https://launcher.mojang.com/v1/objects/e023322a2013912315a2bd1034e6f829a27c76e0/jjs.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/keytool.1": {"downloads": {"lzma": {"sha1": "a78134a4bddd53d684a70aa677e51a215db1c9cb", "size": 20698, "url": "https://launcher.mojang.com/v1/objects/a78134a4bddd53d684a70aa677e51a215db1c9cb/keytool.1"}, "raw": {"sha1": "148583c837eaaf6333ccfd8c9e8df08574e14b0c", "size": 111033, "url": "https://launcher.mojang.com/v1/objects/148583c837eaaf6333ccfd8c9e8df08574e14b0c/keytool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/orbd.1": {"downloads": {"lzma": {"sha1": "326af0dcbff173ef8aee29163dbe146d7389cc3e", "size": 4225, "url": "https://launcher.mojang.com/v1/objects/326af0dcbff173ef8aee29163dbe146d7389cc3e/orbd.1"}, "raw": {"sha1": "95651622d33c08286858ec337edd3ea72acd93dc", "size": 16092, "url": "https://launcher.mojang.com/v1/objects/95651622d33c08286858ec337edd3ea72acd93dc/orbd.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/pack200.1": {"downloads": {"lzma": {"sha1": "e0eedafa748c61a44e5be4355fe9d44b05048e80", "size": 4293, "url": "https://launcher.mojang.com/v1/objects/e0eedafa748c61a44e5be4355fe9d44b05048e80/pack200.1"}, "raw": {"sha1": "aa21a0ab75707f7fc66e83c7a392e69b37ddf80e", "size": 14482, "url": "https://launcher.mojang.com/v1/objects/aa21a0ab75707f7fc66e83c7a392e69b37ddf80e/pack200.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/policytool.1": {"downloads": {"lzma": {"sha1": "3c766ed12dab58166169d35680c392a6be1814a1", "size": 1380, "url": "https://launcher.mojang.com/v1/objects/3c766ed12dab58166169d35680c392a6be1814a1/policytool.1"}, "raw": {"sha1": "80879c74e072a98fad6f32b3283331aaf9bd002f", "size": 4020, "url": "https://launcher.mojang.com/v1/objects/80879c74e072a98fad6f32b3283331aaf9bd002f/policytool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/rmid.1": {"downloads": {"lzma": {"sha1": "1e20779d990beacc32a48237777d670fcc47ca14", "size": 4836, "url": "https://launcher.mojang.com/v1/objects/1e20779d990beacc32a48237777d670fcc47ca14/rmid.1"}, "raw": {"sha1": "7e40cb8003d098d6e36f45640b26f979ac94b5c5", "size": 19715, "url": "https://launcher.mojang.com/v1/objects/7e40cb8003d098d6e36f45640b26f979ac94b5c5/rmid.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/rmiregistry.1": {"downloads": {"lzma": {"sha1": "aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9", "size": 1627, "url": "https://launcher.mojang.com/v1/objects/aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9/rmiregistry.1"}, "raw": {"sha1": "c53c52f3ae7a011c135894c9fc51b741e729c33d", "size": 4557, "url": "https://launcher.mojang.com/v1/objects/c53c52f3ae7a011c135894c9fc51b741e729c33d/rmiregistry.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/servertool.1": {"downloads": {"lzma": {"sha1": "3b9e624e9d1cf2959b438a35061162e2100ddecd", "size": 2626, "url": "https://launcher.mojang.com/v1/objects/3b9e624e9d1cf2959b438a35061162e2100ddecd/servertool.1"}, "raw": {"sha1": "50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e", "size": 9081, "url": "https://launcher.mojang.com/v1/objects/50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e/servertool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/tnameserv.1": {"downloads": {"lzma": {"sha1": "bb3106ff74c60a76de3d20659b9c2128c70f3bf2", "size": 4478, "url": "https://launcher.mojang.com/v1/objects/bb3106ff74c60a76de3d20659b9c2128c70f3bf2/tnameserv.1"}, "raw": {"sha1": "01e714671ecd1167edcb5310b16a9c59c33c3eaa", "size": 17722, "url": "https://launcher.mojang.com/v1/objects/01e714671ecd1167edcb5310b16a9c59c33c3eaa/tnameserv.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/unpack200.1": {"downloads": {"lzma": {"sha1": "c115a881cf800b08df294df55d9f250ae944e33c", "size": 1973, "url": "https://launcher.mojang.com/v1/objects/c115a881cf800b08df294df55d9f250ae944e33c/unpack200.1"}, "raw": {"sha1": "7c882bba0067367a41ad84868d18793b8a7397a3", "size": 5382, "url": "https://launcher.mojang.com/v1/objects/7c882bba0067367a41ad84868d18793b8a7397a3/unpack200.1"}}, "executable": false, "type": "file"}, "man/man1": {"type": "directory"}, "man/man1/java.1": {"downloads": {"lzma": {"sha1": "06a6b0275c202bf698d73ca71f95618d56d81c15", "size": 25796, "url": "https://launcher.mojang.com/v1/objects/06a6b0275c202bf698d73ca71f95618d56d81c15/java.1"}, "raw": {"sha1": "69fec7a341aa91f18dbdcdb95952dede7e1b689a", "size": 124796, "url": "https://launcher.mojang.com/v1/objects/69fec7a341aa91f18dbdcdb95952dede7e1b689a/java.1"}}, "executable": false, "type": "file"}, "man/man1/javaws.1": {"downloads": {"lzma": {"sha1": "4bae251c6dfb5420f56928815cf80d0b6d517a1f", "size": 1759, "url": "https://launcher.mojang.com/v1/objects/4bae251c6dfb5420f56928815cf80d0b6d517a1f/javaws.1"}, "raw": {"sha1": "e61e44e101b1bc119c2d2d4b10320f38b36a8036", "size": 4897, "url": "https://launcher.mojang.com/v1/objects/e61e44e101b1bc119c2d2d4b10320f38b36a8036/javaws.1"}}, "executable": false, "type": "file"}, "man/man1/jjs.1": {"downloads": {"lzma": {"sha1": "29683cf2bd47015c9461b688749ddffd95f6671d", "size": 1881, "url": "https://launcher.mojang.com/v1/objects/29683cf2bd47015c9461b688749ddffd95f6671d/jjs.1"}, "raw": {"sha1": "78d419bd3a7f3e0802d5220e690429194b5d1beb", "size": 4932, "url": "https://launcher.mojang.com/v1/objects/78d419bd3a7f3e0802d5220e690429194b5d1beb/jjs.1"}}, "executable": false, "type": "file"}, "man/man1/keytool.1": {"downloads": {"lzma": {"sha1": "b67e5126d43713ee3675706724b34061578b42db", "size": 19690, "url": "https://launcher.mojang.com/v1/objects/b67e5126d43713ee3675706724b34061578b42db/keytool.1"}, "raw": {"sha1": "4c976f86057ab779763fcfb98f5702ebef47f629", "size": 86925, "url": "https://launcher.mojang.com/v1/objects/4c976f86057ab779763fcfb98f5702ebef47f629/keytool.1"}}, "executable": false, "type": "file"}, "man/man1/orbd.1": {"downloads": {"lzma": {"sha1": "147064d6f7e027002e296bb246ae572d0ce0495b", "size": 3708, "url": "https://launcher.mojang.com/v1/objects/147064d6f7e027002e296bb246ae572d0ce0495b/orbd.1"}, "raw": {"sha1": "64201e1846fcf1dcc45c786ffeab89426d1c7742", "size": 12180, "url": "https://launcher.mojang.com/v1/objects/64201e1846fcf1dcc45c786ffeab89426d1c7742/orbd.1"}}, "executable": false, "type": "file"}, "man/man1/pack200.1": {"downloads": {"lzma": {"sha1": "fe17486bbe9c58cf4182fa056b9cd124e8295607", "size": 3724, "url": "https://launcher.mojang.com/v1/objects/fe17486bbe9c58cf4182fa056b9cd124e8295607/pack200.1"}, "raw": {"sha1": "26826cf52b89924f2d2a60d6cda798891875eae6", "size": 11623, "url": "https://launcher.mojang.com/v1/objects/26826cf52b89924f2d2a60d6cda798891875eae6/pack200.1"}}, "executable": false, "type": "file"}, "man/man1/policytool.1": {"downloads": {"lzma": {"sha1": "bd154e7c39aca71d15b2098c588866f8d95bc743", "size": 1122, "url": "https://launcher.mojang.com/v1/objects/bd154e7c39aca71d15b2098c588866f8d95bc743/policytool.1"}, "raw": {"sha1": "ab296625155d9a2b25ecc2b4feff2f741b3ad136", "size": 3235, "url": "https://launcher.mojang.com/v1/objects/ab296625155d9a2b25ecc2b4feff2f741b3ad136/policytool.1"}}, "executable": false, "type": "file"}, "man/man1/rmid.1": {"downloads": {"lzma": {"sha1": "6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa", "size": 4255, "url": "https://launcher.mojang.com/v1/objects/6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa/rmid.1"}, "raw": {"sha1": "6f10e214d7950a6a8460524e41dc700f112f89e5", "size": 15979, "url": "https://launcher.mojang.com/v1/objects/6f10e214d7950a6a8460524e41dc700f112f89e5/rmid.1"}}, "executable": false, "type": "file"}, "man/man1/rmiregistry.1": {"downloads": {"lzma": {"sha1": "f40dd17e3a734600ad1828b0c42d3a1685c4c520", "size": 1301, "url": "https://launcher.mojang.com/v1/objects/f40dd17e3a734600ad1828b0c42d3a1685c4c520/rmiregistry.1"}, "raw": {"sha1": "d9a3d23fab689df5bb9a792b88f462f939b49f70", "size": 3449, "url": "https://launcher.mojang.com/v1/objects/d9a3d23fab689df5bb9a792b88f462f939b49f70/rmiregistry.1"}}, "executable": false, "type": "file"}, "man/man1/servertool.1": {"downloads": {"lzma": {"sha1": "74f1e10712202cd3ca0ff5833de05b7ee67092e1", "size": 2307, "url": "https://launcher.mojang.com/v1/objects/74f1e10712202cd3ca0ff5833de05b7ee67092e1/servertool.1"}, "raw": {"sha1": "e6c7b510740ac8681a9bfb5f4ee1f0306125b728", "size": 7237, "url": "https://launcher.mojang.com/v1/objects/e6c7b510740ac8681a9bfb5f4ee1f0306125b728/servertool.1"}}, "executable": false, "type": "file"}, "man/man1/tnameserv.1": {"downloads": {"lzma": {"sha1": "4bec7f4e070d023f124f9352a8971d7acd249a15", "size": 3955, "url": "https://launcher.mojang.com/v1/objects/4bec7f4e070d023f124f9352a8971d7acd249a15/tnameserv.1"}, "raw": {"sha1": "a31dbbe800d49cb371fab9a4b73d22c3bf8799ad", "size": 15747, "url": "https://launcher.mojang.com/v1/objects/a31dbbe800d49cb371fab9a4b73d22c3bf8799ad/tnameserv.1"}}, "executable": false, "type": "file"}, "man/man1/unpack200.1": {"downloads": {"lzma": {"sha1": "f8e73863187929debf2ea6dadefb2995ec7917e7", "size": 1672, "url": "https://launcher.mojang.com/v1/objects/f8e73863187929debf2ea6dadefb2995ec7917e7/unpack200.1"}, "raw": {"sha1": "437f7233d738cb9b822e99003127049005663e0f", "size": 4244, "url": "https://launcher.mojang.com/v1/objects/437f7233d738cb9b822e99003127049005663e0f/unpack200.1"}}, "executable": false, "type": "file"}, "plugin": {"type": "directory"}, "plugin/desktop": {"type": "directory"}, "plugin/desktop/sun_java.desktop": {"downloads": {"lzma": {"sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop"}, "raw": {"sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop"}}, "executable": false, "type": "file"}, "plugin/desktop/sun_java.png": {"downloads": {"raw": {"sha1": "699c41e97a35414e72a80327a54d6e14e874e951", "size": 4351, "url": "https://launcher.mojang.com/v1/objects/699c41e97a35414e72a80327a54d6e14e874e951/sun_java.png"}}, "executable": false, "type": "file"}, "release": {"downloads": {"raw": {"sha1": "cb462682644c0275d94a45b759108815f3112064", "size": 424, "url": "https://launcher.mojang.com/v1/objects/cb462682644c0275d94a45b759108815f3112064/release"}}, "executable": false, "type": "file"}}} \ No newline at end of file
diff --git a/launcher/mojang/testdata/inspect/a/b.txt b/launcher/mojang/testdata/inspect/a/b.txt
deleted file mode 100755
index e69de29b..00000000
--- a/launcher/mojang/testdata/inspect/a/b.txt
+++ /dev/null
diff --git a/launcher/mojang/testdata/inspect/a/b/b.txt b/launcher/mojang/testdata/inspect/a/b/b.txt
deleted file mode 120000
index 4e19a044..00000000
--- a/launcher/mojang/testdata/inspect/a/b/b.txt
+++ /dev/null
@@ -1 +0,0 @@
-../b.txt \ No newline at end of file
diff --git a/launcher/mojang/testdata/inspect_win/a/b.txt b/launcher/mojang/testdata/inspect_win/a/b.txt
deleted file mode 100644
index e69de29b..00000000
--- a/launcher/mojang/testdata/inspect_win/a/b.txt
+++ /dev/null
diff --git a/launcher/mojang/testdata/inspect_win/a/b/b.txt b/launcher/mojang/testdata/inspect_win/a/b/b.txt
deleted file mode 100644
index e69de29b..00000000
--- a/launcher/mojang/testdata/inspect_win/a/b/b.txt
+++ /dev/null
diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp
index e6a6adcc..fd3dbedc 100644
--- a/launcher/net/Download.cpp
+++ b/launcher/net/Download.cpp
@@ -60,7 +60,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
dl->m_url = url;
dl->m_options = options;
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
- auto cachedNode = new MetaCacheSink(entry, md5Node);
+ auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal));
dl->m_sink.reset(cachedNode);
return dl;
}
@@ -118,7 +118,7 @@ void Download::executeTask()
}
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame
+ if (APPLICATION->capabilities() & Application::SupportsFlame
&& request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
};
diff --git a/launcher/net/Download.h b/launcher/net/Download.h
index 1d264381..3faa5db5 100644
--- a/launcher/net/Download.h
+++ b/launcher/net/Download.h
@@ -49,7 +49,7 @@ class Download : public NetAction {
public:
using Ptr = shared_qobject_ptr<class Download>;
- enum class Option { NoOptions = 0, AcceptLocalFiles = 1 };
+ enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 };
Q_DECLARE_FLAGS(Options, Option)
protected:
diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp
index 4d86c0b8..9606ddb6 100644
--- a/launcher/net/HttpMetaCache.cpp
+++ b/launcher/net/HttpMetaCache.cpp
@@ -121,6 +121,14 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
SaveEventually();
}
+ // Get rid of old entries, to prevent cache problems
+ auto current_time = QDateTime::currentSecsSinceEpoch();
+ if (entry->isExpired(current_time - ( file_last_changed / 1000 ))) {
+ qWarning() << "Removing cache entry because of old age!";
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+
// entry passed all the checks we cared about.
entry->basePath = getBasePath(base);
return entry;
@@ -221,6 +229,13 @@ void HttpMetaCache::Load()
foo->etag = Json::ensureString(element_obj, "etag");
foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
+
+ foo->makeEternal(Json::ensureBoolean(element_obj, "eternal", false));
+ if (!foo->isEternal()) {
+ foo->current_age = Json::ensureDouble(element_obj, "current_age");
+ foo->max_age = Json::ensureDouble(element_obj, "max_age");
+ }
+
// presumed innocent until closer examination
foo->stale = false;
@@ -240,6 +255,8 @@ void HttpMetaCache::SaveNow()
if (m_index_file.isNull())
return;
+ qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries";
+
QJsonObject toplevel;
Json::writeString(toplevel, "version", "1");
@@ -259,6 +276,12 @@ void HttpMetaCache::SaveNow()
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
if (!entry->remote_changed_timestamp.isEmpty())
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
+ if (entry->isEternal()) {
+ entryObj.insert("eternal", true);
+ } else {
+ entryObj.insert("current_age", QJsonValue(double(entry->current_age)));
+ entryObj.insert("max_age", QJsonValue(double(entry->max_age)));
+ }
entriesArr.append(entryObj);
}
}
diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h
index e944b3d5..c0b12318 100644
--- a/launcher/net/HttpMetaCache.h
+++ b/launcher/net/HttpMetaCache.h
@@ -64,14 +64,31 @@ class MetaEntry {
auto getMD5Sum() -> QString { return md5sum; }
void setMD5Sum(QString md5sum) { this->md5sum = md5sum; }
+ /* Whether the entry expires after some time (false) or not (true). */
+ void makeEternal(bool eternal) { is_eternal = eternal; }
+ [[nodiscard]] bool isEternal() const { return is_eternal; }
+
+ auto getCurrentAge() -> qint64 { return current_age; }
+ void setCurrentAge(qint64 age) { current_age = age; }
+
+ auto getMaximumAge() -> qint64 { return max_age; }
+ void setMaximumAge(qint64 age) { max_age = age; }
+
+ bool isExpired(qint64 offset) { return !is_eternal && (current_age >= max_age - offset); };
+
protected:
QString baseId;
QString basePath;
QString relativePath;
QString md5sum;
QString etag;
+
qint64 local_changed_timestamp = 0;
QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
+ qint64 current_age = 0;
+ qint64 max_age = 0;
+ bool is_eternal = false;
+
bool stale = true;
};
diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp
index f86dd870..5ae53c1c 100644
--- a/launcher/net/MetaCacheSink.cpp
+++ b/launcher/net/MetaCacheSink.cpp
@@ -36,13 +36,18 @@
#include "MetaCacheSink.h"
#include <QFile>
#include <QFileInfo>
-#include "FileSystem.h"
#include "Application.h"
namespace Net {
-MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum)
- :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum)
+/** Maximum time to hold a cache entry
+ * = 1 week in seconds
+ */
+#define MAX_TIME_TO_EXPIRE 1*7*24*60*60
+
+
+MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum, bool is_eternal)
+ :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum), m_is_eternal(is_eternal)
{
addValidator(md5sum);
}
@@ -88,6 +93,40 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply)
}
m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
+
+ { // Cache lifetime
+ if (m_is_eternal) {
+ qDebug() << "[MetaCache] Adding eternal cache entry:" << m_entry->getFullPath();
+ m_entry->makeEternal(true);
+ } else if (reply.hasRawHeader("Cache-Control")) {
+ auto cache_control_header = reply.rawHeader("Cache-Control");
+ // qDebug() << "[MetaCache] Parsing 'Cache-Control' header with" << cache_control_header;
+
+ QRegularExpression max_age_expr("max-age=([0-9]+)");
+ qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong();
+ m_entry->setMaximumAge(max_age);
+
+ } else if (reply.hasRawHeader("Expires")) {
+ auto expires_header = reply.rawHeader("Expires");
+ // qDebug() << "[MetaCache] Parsing 'Expires' header with" << expires_header;
+
+ qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch();
+ m_entry->setMaximumAge(max_age);
+ } else {
+ m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE);
+ }
+
+ if (reply.hasRawHeader("Age")) {
+ auto age_header = reply.rawHeader("Age");
+ // qDebug() << "[MetaCache] Parsing 'Age' header with" << age_header;
+
+ qint64 current_age = age_header.toLongLong();
+ m_entry->setCurrentAge(current_age);
+ } else {
+ m_entry->setCurrentAge(0);
+ }
+ }
+
m_entry->setStale(false);
APPLICATION->metacache()->updateEntry(m_entry);
diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h
index c9f7edfe..f5948085 100644
--- a/launcher/net/MetaCacheSink.h
+++ b/launcher/net/MetaCacheSink.h
@@ -42,7 +42,7 @@
namespace Net {
class MetaCacheSink : public FileSink {
public:
- MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum);
+ MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal = false);
virtual ~MetaCacheSink() = default;
auto hasLocalData() -> bool override;
@@ -54,5 +54,6 @@ class MetaCacheSink : public FileSink {
private:
MetaEntryPtr m_entry;
ChecksumValidator* m_md5Node;
+ bool m_is_eternal;
};
} // namespace Net
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
index 729d4132..d9c4fadc 100644
--- a/launcher/net/NetAction.h
+++ b/launcher/net/NetAction.h
@@ -54,6 +54,8 @@ class NetAction : public Task {
QUrl url() { return m_url; }
auto index() -> int { return m_index_within_job; }
+ void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
+
protected slots:
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0;
virtual void downloadError(QNetworkReply::NetworkError error) = 0;
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
index bab35fa5..8ced1b7e 100644
--- a/launcher/net/NetJob.cpp
+++ b/launcher/net/NetJob.cpp
@@ -35,204 +35,95 @@
*/
#include "NetJob.h"
-#include "Download.h"
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{
- action->m_index_within_job = m_downloads.size();
- m_downloads.append(action);
- part_info pi;
- m_parts_progress.append(pi);
-
- partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress());
-
- if (action->isRunning()) {
- connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); });
- connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); });
- connect(action.get(), &NetAction::aborted, [this, action](){ partAborted(action->index()); });
- connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); });
- connect(action.get(), &NetAction::status, this, &NetJob::status);
- } else {
- m_todo.append(m_parts_progress.size() - 1);
- }
+ action->m_index_within_job = m_queue.size();
+ m_queue.append(action);
+
+ action->setNetwork(m_network);
return true;
}
+void NetJob::startNext()
+{
+ if (m_queue.isEmpty() && m_doing.isEmpty()) {
+ // We're finished, check for failures and retry if we can (up to 3 times)
+ if (!m_failed.isEmpty() && m_try < 3) {
+ m_try += 1;
+ while (!m_failed.isEmpty())
+ m_queue.enqueue(m_failed.take(*m_failed.keyBegin()));
+ }
+ }
+
+ ConcurrentTask::startNext();
+}
+
+auto NetJob::size() const -> int
+{
+ return m_queue.size() + m_doing.size() + m_done.size();
+}
+
auto NetJob::canAbort() const -> bool
{
bool canFullyAbort = true;
// can abort the downloads on the queue?
- for (auto index : m_todo) {
- auto part = m_downloads[index];
+ for (auto part : m_queue)
canFullyAbort &= part->canAbort();
- }
+
// can abort the active downloads?
- for (auto index : m_doing) {
- auto part = m_downloads[index];
+ for (auto part : m_doing)
canFullyAbort &= part->canAbort();
- }
return canFullyAbort;
}
-void NetJob::executeTask()
-{
- // hack that delays early failures so they can be caught easier
- QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection);
-}
-
-auto NetJob::getFailedFiles() -> QStringList
-{
- QStringList failed;
- for (auto index : m_failed) {
- failed.push_back(m_downloads[index]->url().toString());
- }
- failed.sort();
- return failed;
-}
-
auto NetJob::abort() -> bool
{
bool fullyAborted = true;
// fail all downloads on the queue
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet<int> todoSet(m_todo.begin(), m_todo.end());
- m_failed.unite(todoSet);
-#else
- m_failed.unite(m_todo.toSet());
-#endif
- m_todo.clear();
+ for (auto task : m_queue)
+ m_failed.insert(task.get(), task);
+ m_queue.clear();
// abort active downloads
auto toKill = m_doing.values();
- for (auto index : toKill) {
- auto part = m_downloads[index];
+ for (auto part : toKill) {
fullyAborted &= part->abort();
}
- return fullyAborted;
-}
-
-void NetJob::partSucceeded(int index)
-{
- // do progress. all slots are 1 in size at least
- auto& slot = m_parts_progress[index];
- partProgress(index, slot.total_progress, slot.total_progress);
+ if (fullyAborted)
+ emitAborted();
+ else
+ emitFailed(tr("Failed to abort all tasks in the NetJob!"));
- m_doing.remove(index);
- m_done.insert(index);
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
+ return fullyAborted;
}
-void NetJob::partFailed(int index)
+auto NetJob::getFailedActions() -> QList<NetAction*>
{
- m_doing.remove(index);
-
- auto& slot = m_parts_progress[index];
- // Can try 3 times before failing by definitive
- if (slot.failures == 3) {
- m_failed.insert(index);
- } else {
- slot.failures++;
- m_todo.enqueue(index);
+ QList<NetAction*> failed;
+ for (auto index : m_failed) {
+ failed.push_back(dynamic_cast<NetAction*>(index.get()));
}
-
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
-}
-
-void NetJob::partAborted(int index)
-{
- m_aborted = true;
-
- m_doing.remove(index);
- m_failed.insert(index);
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
+ return failed;
}
-void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
+auto NetJob::getFailedFiles() -> QList<QString>
{
- auto& slot = m_parts_progress[index];
- slot.current_progress = bytesReceived;
- slot.total_progress = bytesTotal;
-
- int done = m_done.size();
- int doing = m_doing.size();
- int all = m_parts_progress.size();
-
- qint64 bytesAll = 0;
- qint64 bytesTotalAll = 0;
- for (auto& partIdx : m_doing) {
- auto part = m_parts_progress[partIdx];
- // do not count parts with unknown/nonsensical total size
- if (part.total_progress <= 0) {
- continue;
- }
- bytesAll += part.current_progress;
- bytesTotalAll += part.total_progress;
- }
-
- qint64 inprogress = (bytesTotalAll == 0) ? 0 : (bytesAll * 1000) / bytesTotalAll;
- auto current = done * 1000 + doing * inprogress;
- auto current_total = all * 1000;
- // HACK: make sure it never jumps backwards.
- // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress
- if (m_current_progress == 1000) {
- m_current_progress = inprogress;
- }
- if (m_current_progress > current) {
- current = m_current_progress;
+ QList<QString> failed;
+ for (auto index : m_failed) {
+ failed.append(static_cast<NetAction*>(index.get())->url().toString());
}
- m_current_progress = current;
- setProgress(current, current_total);
+ return failed;
}
-void NetJob::startMoreParts()
+void NetJob::updateState()
{
- if (!isRunning()) {
- // this actually makes sense. You can put running m_downloads into a NetJob and then not start it until much later.
- return;
- }
-
- // OK. We are actively processing tasks, proceed.
- // Check for final conditions if there's nothing in the queue.
- if (!m_todo.size()) {
- if (!m_doing.size()) {
- if (!m_failed.size()) {
- emitSucceeded();
- } else if (m_aborted) {
- emitAborted();
- } else {
- emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n")));
- }
- }
- return;
- }
-
- // There's work to do, try to start more parts, to a maximum of 6 concurrent ones.
- while (m_doing.size() < 6) {
- if (m_todo.size() == 0)
- return;
- int doThis = m_todo.dequeue();
- m_doing.insert(doThis);
-
- auto part = m_downloads[doThis];
-
- // connect signals :D
- connect(part.get(), &NetAction::succeeded, this, [this, part]{ partSucceeded(part->index()); });
- connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); });
- connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); });
- connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); });
- connect(part.get(), &NetAction::status, this, &NetJob::status);
-
- part->startAction(m_network);
- }
+ emit progress(m_done.count(), m_total_size);
+ setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
+ .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h
index 63c1cf51..cd5d5e48 100644
--- a/launcher/net/NetJob.h
+++ b/launcher/net/NetJob.h
@@ -39,64 +39,40 @@
#include <QObject>
#include "NetAction.h"
-#include "tasks/Task.h"
+#include "tasks/ConcurrentTask.h"
// Those are included so that they are also included by anyone using NetJob
#include "net/Download.h"
#include "net/HttpMetaCache.h"
-class NetJob : public Task {
+class NetJob : public ConcurrentTask {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<NetJob>;
- explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : Task(), m_network(network)
- {
- setObjectName(job_name);
- }
- virtual ~NetJob() = default;
+ explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : ConcurrentTask(nullptr, job_name), m_network(network) {}
+ ~NetJob() override = default;
- void executeTask() override;
+ void startNext() override;
- auto canAbort() const -> bool override;
+ auto size() const -> int;
+ auto canAbort() const -> bool override;
auto addNetAction(NetAction::Ptr action) -> bool;
- auto operator[](int index) -> NetAction::Ptr { return m_downloads[index]; }
- auto at(int index) -> const NetAction::Ptr { return m_downloads.at(index); }
- auto size() const -> int { return m_downloads.size(); }
- auto first() -> NetAction::Ptr { return m_downloads.size() != 0 ? m_downloads[0] : NetAction::Ptr{}; }
-
- auto getFailedFiles() -> QStringList;
+ auto getFailedActions() -> QList<NetAction*>;
+ auto getFailedFiles() -> QList<QString>;
public slots:
// Qt can't handle auto at the start for some reason?
bool abort() override;
- private slots:
- void startMoreParts();
-
- void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
- void partSucceeded(int index);
- void partFailed(int index);
- void partAborted(int index);
+ protected:
+ void updateState() override;
private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
- struct part_info {
- qint64 current_progress = 0;
- qint64 total_progress = 1;
- int failures = 0;
- };
-
- QList<NetAction::Ptr> m_downloads;
- QList<part_info> m_parts_progress;
- QQueue<int> m_todo;
- QSet<int> m_doing;
- QSet<int> m_done;
- QSet<int> m_failed;
- qint64 m_current_progress = 0;
- bool m_aborted = false;
+ int m_try = 1;
};
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
index cfda4b4e..f3b19022 100644
--- a/launcher/net/Upload.cpp
+++ b/launcher/net/Upload.cpp
@@ -216,7 +216,7 @@ namespace Net {
}
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame
+ if (APPLICATION->capabilities() & Application::SupportsFlame
&& request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
}
diff --git a/launcher/settings/INIFile_test.cpp b/launcher/settings/INIFile_test.cpp
deleted file mode 100644
index d23f9fdf..00000000
--- a/launcher/settings/INIFile_test.cpp
+++ /dev/null
@@ -1,62 +0,0 @@
-#include <QTest>
-
-#include "settings/INIFile.h"
-
-class IniFileTest : public QObject
-{
- Q_OBJECT
-private
-slots:
- void initTestCase()
- {
-
- }
- void cleanupTestCase()
- {
-
- }
-
- void test_Escape_data()
- {
- QTest::addColumn<QString>("through");
-
- QTest::newRow("unix path") << "/abc/def/ghi/jkl";
- QTest::newRow("windows path") << "C:\\Program files\\terrible\\name\\of something\\";
- QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet.";
- QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet.";
- QTest::newRow("Escape sequences 2") << "\"\n\n\"";
- QTest::newRow("Hashtags") << "some data#something";
- }
- void test_Escape()
- {
- QFETCH(QString, through);
-
- QString there = INIFile::escape(through);
- QString back = INIFile::unescape(there);
-
- QCOMPARE(back, through);
- }
-
- void test_SaveLoad()
- {
- QString a = "a";
- QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment";
- QString filename = "test_SaveLoad.ini";
-
- // save
- INIFile f;
- f.set("a", a);
- f.set("b", b);
- f.saveFile(filename);
-
- // load
- INIFile f2;
- f2.loadFile(filename);
- QCOMPARE(a, f2.get("a","NOT SET").toString());
- QCOMPARE(b, f2.get("b","NOT SET").toString());
- }
-};
-
-QTEST_GUILESS_MAIN(IniFileTest)
-
-#include "INIFile_test.moc"
diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h
index 3d61e707..6200bc3a 100644
--- a/launcher/settings/SettingsObject.h
+++ b/launcher/settings/SettingsObject.h
@@ -25,6 +25,7 @@ class Setting;
class SettingsObject;
typedef std::shared_ptr<SettingsObject> SettingsObjectPtr;
+typedef std::weak_ptr<SettingsObject> SettingsObjectWeakPtr;
/*!
* \brief The SettingsObject handles communicating settings between the application and a
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index b88cfb13..ce08a6a2 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -1,10 +1,11 @@
#include "ConcurrentTask.h"
#include <QDebug>
+#include <QCoreApplication>
ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent)
: Task(parent), m_name(task_name), m_total_max_size(max_concurrent)
-{}
+{ setObjectName(task_name); }
ConcurrentTask::~ConcurrentTask()
{
@@ -36,31 +37,40 @@ void ConcurrentTask::executeTask()
{
m_total_size = m_queue.size();
- for (int i = 0; i < m_total_max_size; i++)
- startNext();
+ // Start the least amount of tasks needed, but at least one
+ int num_starts = std::max(1, std::min(m_total_max_size, m_total_size));
+ for (int i = 0; i < num_starts; i++) {
+ QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
+ }
}
bool ConcurrentTask::abort()
{
+ m_queue.clear();
+ m_aborted = true;
+
if (m_doing.isEmpty()) {
// Don't call emitAborted() here, we want to bypass the 'is the task running' check
emit aborted();
emit finished();
- m_aborted = true;
return true;
}
- m_queue.clear();
+ bool suceedeed = true;
- m_aborted = true;
- for (auto task : m_doing)
- m_aborted &= task->abort();
+ QMutableHashIterator<Task*, Task::Ptr> doing_iter(m_doing);
+ while (doing_iter.hasNext()) {
+ auto task = doing_iter.next();
+ suceedeed &= (task.value())->abort();
+ }
- if (m_aborted)
+ if (suceedeed)
emitAborted();
+ else
+ emitFailed(tr("Failed to abort all running tasks."));
- return m_aborted;
+ return suceedeed;
}
void ConcurrentTask::startNext()
@@ -68,7 +78,7 @@ void ConcurrentTask::startNext()
if (m_aborted || m_doing.count() > m_total_max_size)
return;
- if (m_queue.isEmpty() && m_doing.isEmpty()) {
+ if (m_queue.isEmpty() && m_doing.isEmpty() && !wasSuccessful()) {
emitSucceeded();
return;
}
@@ -91,6 +101,8 @@ void ConcurrentTask::startNext()
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
updateState();
+ QCoreApplication::processEvents();
+
next->start();
}
@@ -127,11 +139,6 @@ void ConcurrentTask::subTaskStatus(const QString& msg)
void ConcurrentTask::subTaskProgress(qint64 current, qint64 total)
{
- if (total == 0) {
- setProgress(0, 100);
- return;
- }
-
m_stepProgress = current;
m_stepTotalProgress = total;
}
diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h
index 5898899d..f1279d32 100644
--- a/launcher/tasks/ConcurrentTask.h
+++ b/launcher/tasks/ConcurrentTask.h
@@ -9,7 +9,9 @@ class ConcurrentTask : public Task {
Q_OBJECT
public:
explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6);
- virtual ~ConcurrentTask();
+ ~ConcurrentTask() override;
+
+ bool canAbort() const override { return true; }
inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; };
auto getStepProgress() const -> qint64 override;
diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp
index 6e853568..5ad6181f 100644
--- a/launcher/tasks/MultipleOptionsTask.cpp
+++ b/launcher/tasks/MultipleOptionsTask.cpp
@@ -6,43 +6,22 @@ MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_na
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())) {
+ if (m_done.size() != m_failed.size()) {
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());
+ if (m_queue.isEmpty()) {
+ emitFailed(tr("All attempts have failed!"));
+ qWarning() << "All attempts have failed!";
+ return;
+ }
- next->start();
+ ConcurrentTask::startNext();
}
-void MultipleOptionsTask::subTaskFailed(QString const& reason)
+void MultipleOptionsTask::updateState()
{
- 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();
+ setProgress(m_done.count(), m_total_size);
+ setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h
index 7c508b00..db7d4d9a 100644
--- a/launcher/tasks/MultipleOptionsTask.h
+++ b/launcher/tasks/MultipleOptionsTask.h
@@ -5,15 +5,13 @@
/* 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
-{
+class MultipleOptionsTask : public SequentialTask {
Q_OBJECT
-public:
- explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = "");
- virtual ~MultipleOptionsTask() = default;
+ public:
+ explicit MultipleOptionsTask(QObject* parent = nullptr, const QString& task_name = "");
+ ~MultipleOptionsTask() override = default;
-private
-slots:
+ private slots:
void startNext() override;
- void subTaskFailed(const QString &msg) override;
+ void updateState() override;
};
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index f1e1a889..a34137cb 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -2,107 +2,21 @@
#include <QDebug>
-SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {}
-
-SequentialTask::~SequentialTask()
-{
- for(auto task : m_queue){
- if(task)
- task->deleteLater();
- }
-}
-
-auto SequentialTask::getStepProgress() const -> qint64
-{
- return m_stepProgress;
-}
-
-auto SequentialTask::getStepTotalProgress() const -> qint64
-{
- return m_stepTotalProgress;
-}
-
-void SequentialTask::addTask(Task::Ptr task)
-{
- m_queue.append(task);
-}
-
-void SequentialTask::executeTask()
-{
- m_currentIndex = -1;
- startNext();
-}
-
-bool SequentialTask::abort()
-{
- if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) {
- if(m_currentIndex == -1) {
- // Don't call emitAborted() here, we want to bypass the 'is the task running' check
- emit aborted();
- emit finished();
- }
-
- m_aborted = true;
- return true;
- }
-
- bool succeeded = m_queue[m_currentIndex]->abort();
- m_aborted = succeeded;
-
- if (succeeded)
- emitAborted();
-
- return succeeded;
-}
+SequentialTask::SequentialTask(QObject* parent, QString task_name) : ConcurrentTask(parent, task_name, 1) {}
void SequentialTask::startNext()
{
- 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();
+ if (m_failed.size() > 0) {
+ emitFailed(tr("One of the tasks failed!"));
+ qWarning() << m_failed.constBegin()->get()->failReason();
return;
}
- Task::Ptr next = m_queue[m_currentIndex];
-
- connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString)));
- connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext()));
-
- connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString)));
- connect(next.get(), SIGNAL(stepStatus(QString)), this, SLOT(subTaskStatus(QString)));
-
- connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64)));
- 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();
+ ConcurrentTask::startNext();
}
-void SequentialTask::subTaskFailed(const QString& msg)
+void SequentialTask::updateState()
{
- emitFailed(msg);
-}
-void SequentialTask::subTaskStatus(const QString& msg)
-{
- setStepStatus(msg);
-}
-void SequentialTask::subTaskProgress(qint64 current, qint64 total)
-{
- if (total == 0) {
- setProgress(0, 100);
- return;
- }
-
- m_stepProgress = current;
- m_stepTotalProgress = total;
+ setProgress(m_done.count(), m_total_size);
+ setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h
index f5a58b1b..5eace96e 100644
--- a/launcher/tasks/SequentialTask.h
+++ b/launcher/tasks/SequentialTask.h
@@ -1,49 +1,21 @@
#pragma once
-#include "Task.h"
-#include "QObjectPtr.h"
-
-#include <QQueue>
-
-class SequentialTask : public Task
-{
+#include "ConcurrentTask.h"
+
+/** A concurrent task that only allows one concurrent task :)
+ *
+ * This should be used when there's a need to maintain a strict ordering of task executions, and
+ * the starting of a task is contingent on the success of the previous one.
+ *
+ * See MultipleOptionsTask if that's not the case.
+ */
+class SequentialTask : public ConcurrentTask {
Q_OBJECT
-public:
- explicit SequentialTask(QObject *parent = nullptr, const QString& task_name = "");
- virtual ~SequentialTask();
-
- inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; };
- auto getStepProgress() const -> qint64 override;
- auto getStepTotalProgress() const -> qint64 override;
-
- inline auto getStepStatus() const -> QString override { return m_step_status; }
-
- void addTask(Task::Ptr task);
-
-public slots:
- bool abort() override;
-
-protected
-slots:
- 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); };
-
-protected:
- QString m_name;
- QString m_step_status;
-
- QQueue<Task::Ptr > m_queue;
- int m_currentIndex;
-
- qint64 m_stepProgress = 0;
- qint64 m_stepTotalProgress = 100;
+ public:
+ explicit SequentialTask(QObject* parent = nullptr, QString task_name = "");
+ ~SequentialTask() override = default;
- bool m_aborted = false;
+ protected:
+ void startNext() override;
+ void updateState() override;
};
diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp
index bb71b98c..b4babdd4 100644
--- a/launcher/tasks/Task.cpp
+++ b/launcher/tasks/Task.cpp
@@ -37,8 +37,9 @@
#include <QDebug>
-Task::Task(QObject *parent) : QObject(parent)
+Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug)
{
+ setAutoDelete(false);
}
void Task::setStatus(const QString &new_status)
@@ -63,27 +64,32 @@ void Task::start()
{
case State::Inactive:
{
- qDebug() << "Task" << describe() << "starting for the first time";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "starting for the first time";
break;
}
case State::AbortedByUser:
{
- qDebug() << "Task" << describe() << "restarting for after being aborted by user";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "restarting for after being aborted by user";
break;
}
case State::Failed:
{
- qDebug() << "Task" << describe() << "restarting for after failing at first";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "restarting for after failing at first";
break;
}
case State::Succeeded:
{
- qDebug() << "Task" << describe() << "restarting for after succeeding at first";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "restarting for after succeeding at first";
break;
}
case State::Running:
{
- qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
+ if (m_show_debug)
+ qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
return;
}
}
@@ -118,7 +124,8 @@ void Task::emitAborted()
}
m_state = State::AbortedByUser;
m_failReason = "Aborted.";
- qDebug() << "Task" << describe() << "aborted.";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "aborted.";
emit aborted();
emit finished();
}
@@ -132,7 +139,8 @@ void Task::emitSucceeded()
return;
}
m_state = State::Succeeded;
- qDebug() << "Task" << describe() << "succeeded";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "succeeded";
emit succeeded();
emit finished();
}
diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h
index aafaf68c..2baf0188 100644
--- a/launcher/tasks/Task.h
+++ b/launcher/tasks/Task.h
@@ -35,9 +35,11 @@
#pragma once
+#include <QRunnable>
+
#include "QObjectPtr.h"
-class Task : public QObject {
+class Task : public QObject, public QRunnable {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Task>;
@@ -45,7 +47,7 @@ class Task : public QObject {
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
public:
- explicit Task(QObject* parent = 0);
+ explicit Task(QObject* parent = 0, bool show_debug_log = true);
virtual ~Task() = default;
bool isRunning() const;
@@ -95,6 +97,9 @@ class Task : public QObject {
void stepStatus(QString status);
public slots:
+ // QRunnable's interface
+ void run() override { start(); }
+
virtual void start();
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
@@ -117,4 +122,7 @@ class Task : public QObject {
QString m_status;
int m_progress = 0;
int m_progressTotal = 100;
+
+ // TODO: Nuke in favor of QLoggingCategory
+ bool m_show_debug = true;
};
diff --git a/launcher/tasks/Task_test.cpp b/launcher/tasks/Task_test.cpp
deleted file mode 100644
index ef153a6a..00000000
--- a/launcher/tasks/Task_test.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-#include <QTest>
-
-#include "Task.h"
-
-/* Does nothing. Only used for testing. */
-class BasicTask : public Task {
- Q_OBJECT
-
- friend class TaskTest;
-
- private:
- void executeTask() override {};
-};
-
-/* Does nothing. Only used for testing. */
-class BasicTask_MultiStep : public Task {
- Q_OBJECT
-
- friend class TaskTest;
-
- private:
- auto isMultiStep() const -> bool override { return true; }
-
- void executeTask() override {};
-};
-
-class TaskTest : public QObject {
- Q_OBJECT
-
- private slots:
- void test_SetStatus_NoMultiStep(){
- BasicTask t;
- QString status {"test status"};
-
- t.setStatus(status);
-
- QCOMPARE(t.getStatus(), status);
- QCOMPARE(t.getStepStatus(), status);
- }
-
- void test_SetStatus_MultiStep(){
- BasicTask_MultiStep t;
- QString status {"test status"};
-
- t.setStatus(status);
-
- QCOMPARE(t.getStatus(), status);
- // Even though it is multi step, it does not override the getStepStatus method,
- // so it should remain the same.
- QCOMPARE(t.getStepStatus(), status);
- }
-
- void test_SetProgress(){
- BasicTask t;
- int current = 42;
- int total = 207;
-
- t.setProgress(current, total);
-
- QCOMPARE(t.getProgress(), current);
- QCOMPARE(t.getTotalProgress(), total);
- }
-};
-
-QTEST_GUILESS_MAIN(TaskTest)
-
-#include "Task_test.moc"
diff --git a/launcher/testdata/FileSystem-test_createShortcut-unix b/launcher/testdata/FileSystem-test_createShortcut-unix
deleted file mode 100755
index 1ce3a2bd..00000000
--- a/launcher/testdata/FileSystem-test_createShortcut-unix
+++ /dev/null
@@ -1,6 +0,0 @@
-[Desktop Entry]
-Type=Application
-TryExec=asdfDest
-Exec=asdfDest 'arg1' 'arg2'
-Name=asdf
-Icon=
diff --git a/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt b/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt
deleted file mode 100644
index 8d1c8b69..00000000
--- a/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/launcher/testdata/test_folder/pack.mcmeta b/launcher/testdata/test_folder/pack.mcmeta
deleted file mode 100644
index 67ee0434..00000000
--- a/launcher/testdata/test_folder/pack.mcmeta
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "pack": {
- "pack_format": 1,
- "description": "Some resource pack maybe"
- }
-}
diff --git a/launcher/testdata/test_folder/pack.nfo b/launcher/testdata/test_folder/pack.nfo
deleted file mode 100644
index 8d1c8b69..00000000
--- a/launcher/testdata/test_folder/pack.nfo
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index d58f158e..299401f5 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);
@@ -1445,6 +1465,7 @@ void MainWindow::updateNewsLabel()
{
newsLabel->setText(tr("Loading news..."));
newsLabel->setEnabled(false);
+ ui->actionMoreNews->setVisible(false);
}
else
{
@@ -1453,11 +1474,13 @@ void MainWindow::updateNewsLabel()
{
newsLabel->setText(entries[0]->title);
newsLabel->setEnabled(true);
+ ui->actionMoreNews->setVisible(true);
}
else
{
newsLabel->setText(tr("No news available."));
newsLabel->setEnabled(false);
+ ui->actionMoreNews->setVisible(false);
}
}
}
@@ -1832,6 +1855,11 @@ void MainWindow::deleteGroup()
}
}
+void MainWindow::undoTrashInstance()
+{
+ APPLICATION->instances()->undoTrashInstance();
+}
+
void MainWindow::on_actionViewInstanceFolder_triggered()
{
QString str = APPLICATION->settings()->get("InstanceDir").toString();
@@ -1957,7 +1985,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/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
index c5367d5b..743c34f1 100644
--- a/launcher/ui/dialogs/AboutDialog.cpp
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -147,10 +147,15 @@ AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDia
else
ui->platformLabel->setVisible(false);
- if (BuildConfig.VERSION_BUILD >= 0)
- ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD));
+ if (!BuildConfig.GIT_COMMIT.isEmpty())
+ ui->commitLabel->setText(tr("Commit: %1").arg(BuildConfig.GIT_COMMIT));
else
- ui->buildNumLabel->setVisible(false);
+ ui->commitLabel->setVisible(false);
+
+ if (!BuildConfig.BUILD_DATE.isEmpty())
+ ui->buildDateLabel->setText(tr("Build date: %1").arg(BuildConfig.BUILD_DATE));
+ else
+ ui->buildDateLabel->setVisible(false);
if (!BuildConfig.VERSION_CHANNEL.isEmpty())
ui->channelLabel->setText(tr("Channel") +": " + BuildConfig.VERSION_CHANNEL);
diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui
index 6323992b..e0429321 100644
--- a/launcher/ui/dialogs/AboutDialog.ui
+++ b/launcher/ui/dialogs/AboutDialog.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>783</width>
- <height>843</height>
+ <width>573</width>
+ <height>600</height>
</rect>
</property>
<property name="minimumSize">
@@ -184,12 +184,28 @@
</widget>
</item>
<item>
- <widget class="QLabel" name="buildNumLabel">
+ <widget class="QLabel" name="buildDateLabel">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
- <string>Build Number:</string>
+ <string>Build Date:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="commitLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>Commit:</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
new file mode 100644
index 00000000..fe87b517
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -0,0 +1,28 @@
+#include "BlockedModsDialog.h"
+#include "ui_BlockedModsDialog.h"
+#include <QPushButton>
+#include <QDialogButtonBox>
+#include <QDesktopServices>
+
+
+BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls) :
+ QDialog(parent), ui(new Ui::BlockedModsDialog), urls(urls) {
+ ui->setupUi(this);
+
+ auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole);
+ connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll);
+
+ this->setWindowTitle(title);
+ ui->label->setText(text);
+ ui->textBrowser->setText(body);
+}
+
+BlockedModsDialog::~BlockedModsDialog() {
+ delete ui;
+}
+
+void BlockedModsDialog::openAll() {
+ for(auto &url : urls) {
+ QDesktopServices::openUrl(url);
+ }
+}
diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h
new file mode 100644
index 00000000..5f5bd61b
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <QDialog>
+
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class BlockedModsDialog; }
+QT_END_NAMESPACE
+
+class BlockedModsDialog : public QDialog {
+Q_OBJECT
+
+public:
+ BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls);
+
+ ~BlockedModsDialog() override;
+
+private:
+ Ui::BlockedModsDialog *ui;
+ const QList<QUrl> &urls;
+ void openAll();
+};
diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui
new file mode 100644
index 00000000..f4ae95b6
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BlockedModsDialog</class>
+ <widget class="QDialog" name="BlockedModsDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>455</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">BlockedModsDialog</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="acceptRichText">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>BlockedModsDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>BlockedModsDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp
index 194315a7..30394b72 100644
--- a/launcher/ui/dialogs/LoginDialog.cpp
+++ b/launcher/ui/dialogs/LoginDialog.cpp
@@ -115,5 +115,5 @@ MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
{
return dlg.m_account;
}
- return 0;
+ return nullptr;
}
diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp
index b11b6980..be49babb 100644
--- a/launcher/ui/dialogs/MSALoginDialog.cpp
+++ b/launcher/ui/dialogs/MSALoginDialog.cpp
@@ -169,5 +169,5 @@ MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg)
{
return dlg.m_account;
}
- return 0;
+ return nullptr;
}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
index e4fc3ecc..d740c8cb 100644
--- a/launcher/ui/dialogs/ModDownloadDialog.cpp
+++ b/launcher/ui/dialogs/ModDownloadDialog.cpp
@@ -19,36 +19,33 @@
#include "ModDownloadDialog.h"
#include <BaseVersion.h>
-#include <icons/IconList.h>
#include <InstanceList.h>
+#include <icons/IconList.h>
#include "Application.h"
-#include "ProgressDialog.h"
#include "ReviewMessageBox.h"
+#include <QDialogButtonBox>
#include <QLayout>
#include <QPushButton>
#include <QValidator>
-#include <QDialogButtonBox>
-#include "ui/widgets/PageContainer.h"
-#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
#include "ModDownloadTask.h"
+#include "ui/pages/modplatform/flame/FlameModPage.h"
+#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
+#include "ui/widgets/PageContainer.h"
-
-ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent,
- BaseInstance *instance)
- : QDialog(parent), mods(mods), m_instance(instance)
+ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance)
+ : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance)
{
setObjectName(QStringLiteral("ModDownloadDialog"));
-
- resize(std::max(0.5*parent->width(), 400.0), std::max(0.75*parent->height(), 400.0));
-
- m_verticalLayout = new QVBoxLayout(this);
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
+
setWindowIcon(APPLICATION->getThemedIcon("new"));
- // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below.
+ // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not
+ // move this below.
m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_container = new PageContainer(this);
@@ -58,12 +55,17 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
m_container->addButtons(m_buttons);
+ connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged);
+
// Bonk Qt over its stupid head and make sure it understands which button is the default one...
// See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
OkButton->setEnabled(false);
OkButton->setDefault(true);
OkButton->setAutoDefault(true);
+ OkButton->setText(tr("Review and confirm"));
+ OkButton->setShortcut(tr("Ctrl+Return"));
+ OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return"));
connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
@@ -78,7 +80,9 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
QMetaObject::connectSlotsByName(this);
setWindowModality(Qt::WindowModal);
- setWindowTitle("Download mods");
+ setWindowTitle(dialogTitle());
+
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray()));
}
QString ModDownloadDialog::dialogTitle()
@@ -88,6 +92,7 @@ QString ModDownloadDialog::dialogTitle()
void ModDownloadDialog::reject()
{
+ APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::reject();
}
@@ -114,21 +119,22 @@ void ModDownloadDialog::confirm()
void ModDownloadDialog::accept()
{
+ APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::accept();
}
-QList<BasePage *> ModDownloadDialog::getPages()
+QList<BasePage*> ModDownloadDialog::getPages()
{
- QList<BasePage *> pages;
+ QList<BasePage*> pages;
- pages.append(new ModrinthModPage(this, m_instance));
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
- pages.append(new FlameModPage(this, m_instance));
+ pages.append(ModrinthModPage::create(this, m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameModPage::create(this, m_instance));
return pages;
}
-void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task)
+void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task)
{
removeSelectedMod(name);
modTask.insert(name, task);
@@ -136,16 +142,16 @@ void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* tas
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
-void ModDownloadDialog::removeSelectedMod(const QString &name)
+void ModDownloadDialog::removeSelectedMod(QString name)
{
- if(modTask.contains(name))
+ if (modTask.contains(name))
delete modTask.find(name).value();
modTask.remove(name);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
-bool ModDownloadDialog::isModSelected(const QString &name, const QString& filename) const
+bool ModDownloadDialog::isModSelected(QString name, QString filename) const
{
// FIXME: Is there a way to check for versions without checking the filename
// as a heuristic, other than adding such info to ModDownloadTask itself?
@@ -153,16 +159,31 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena
return iter != modTask.end() && (iter.value()->getFilename() == filename);
}
-bool ModDownloadDialog::isModSelected(const QString &name) const
+bool ModDownloadDialog::isModSelected(QString name) const
{
auto iter = modTask.find(name);
return iter != modTask.end();
}
-ModDownloadDialog::~ModDownloadDialog()
+const QList<ModDownloadTask*> ModDownloadDialog::getTasks()
{
+ return modTask.values();
}
-const QList<ModDownloadTask*> ModDownloadDialog::getTasks() {
- return modTask.values();
+void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
+{
+ auto* prev_page = dynamic_cast<ModPage*>(previous);
+ if (!prev_page) {
+ qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!";
+ return;
+ }
+
+ auto* selected_page = dynamic_cast<ModPage*>(selected);
+ if (!selected_page) {
+ qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!";
+ return;
+ }
+
+ // Same effect as having a global search bar
+ selected_page->setSearchTerm(prev_page->getSearchTerm());
}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h
index 1fa1f058..18a5f0f3 100644
--- a/launcher/ui/dialogs/ModDownloadDialog.h
+++ b/launcher/ui/dialogs/ModDownloadDialog.h
@@ -21,11 +21,9 @@
#include <QDialog>
#include <QVBoxLayout>
-#include "BaseVersion.h"
-#include "ui/pages/BasePageProvider.h"
-#include "minecraft/mod/ModFolderModel.h"
#include "ModDownloadTask.h"
-#include "ui/pages/modplatform/flame/FlameModPage.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "ui/pages/BasePageProvider.h"
namespace Ui
{
@@ -36,21 +34,21 @@ class PageContainer;
class QDialogButtonBox;
class ModrinthModPage;
-class ModDownloadDialog : public QDialog, public BasePageProvider
+class ModDownloadDialog final : public QDialog, public BasePageProvider
{
Q_OBJECT
public:
- explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, BaseInstance *instance);
- ~ModDownloadDialog();
+ explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
+ ~ModDownloadDialog() override = default;
QString dialogTitle() override;
- QList<BasePage *> getPages() override;
+ QList<BasePage*> getPages() override;
- void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr);
- void removeSelectedMod(const QString & name = QString());
- bool isModSelected(const QString & name, const QString & filename) const;
- bool isModSelected(const QString & name) const;
+ void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
+ void removeSelectedMod(QString name = QString());
+ bool isModSelected(QString name, QString filename) const;
+ bool isModSelected(QString name) const;
const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &mods;
@@ -60,6 +58,9 @@ public slots:
void accept() override;
void reject() override;
+private slots:
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
private:
Ui::ModDownloadDialog *ui = nullptr;
PageContainer * m_container = nullptr;
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
index d73c8ebb..4171586e 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -36,7 +36,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
ModUpdateDialog::ModUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ModFolderModel> mods,
- QList<Mod::Ptr>& search_for)
+ QList<Mod*>& search_for)
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
, m_parent(parent)
, m_mod_model(mods)
@@ -226,9 +226,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool
};
for (auto candidate : m_candidates) {
- auto* candidate_ptr = candidate.get();
if (candidate->status() != ModStatus::NoMetadata) {
- onMetadataEnsured(candidate_ptr);
+ onMetadataEnsured(candidate);
continue;
}
@@ -236,7 +235,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
continue;
if (confirm_rest) {
- addToTmp(candidate_ptr, provider_rest);
+ addToTmp(candidate, provider_rest);
should_try_others.insert(candidate->internal_id(), try_others_rest);
continue;
}
@@ -261,7 +260,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
should_try_others.insert(candidate->internal_id(), response.try_others);
if (confirmed)
- addToTmp(candidate_ptr, response.chosen);
+ addToTmp(candidate, response.chosen);
}
if (!modrinth_tmp.empty()) {
@@ -270,6 +269,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
});
+
+ if (modrinth_task->getHashingTask())
+ seq.addTask(modrinth_task->getHashingTask());
+
seq.addTask(modrinth_task);
}
@@ -279,6 +282,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
});
+
+ if (flame_task->getHashingTask())
+ seq.addTask(flame_task->getHashingTask());
+
seq.addTask(flame_task);
}
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
index 76aaab36..bd486f0d 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.h
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -19,7 +19,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
explicit ModUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ModFolderModel> mod_model,
- QList<Mod::Ptr>& search_for);
+ QList<Mod*>& search_for);
void checkCandidates();
@@ -46,7 +46,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
const std::shared_ptr<ModFolderModel> m_mod_model;
- QList<Mod::Ptr>& m_candidates;
+ QList<Mod*>& m_candidates;
QList<Mod*> m_modrinth_to_update;
QList<Mod*> m_flame_to_update;
diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
index 35bba9be..675f8b15 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -157,7 +157,7 @@ QList<BasePage *> NewInstanceDialog::getPages()
pages.append(new VanillaPage(this));
pages.append(importPage);
pages.append(new AtlPage(this));
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(new FlamePage(this));
pages.append(new FtbPage(this));
pages.append(new LegacyFTB::Page(this));
diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp
index 4f3d8be4..a69537ab 100644
--- a/launcher/ui/dialogs/OfflineLoginDialog.cpp
+++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp
@@ -103,5 +103,5 @@ MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget *parent, QString msg)
{
return dlg.m_account;
}
- return 0;
+ return nullptr;
}
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
index a79bc837..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()
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index e664e566..7c25c91c 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -1,11 +1,16 @@
#include "ReviewMessageBox.h"
#include "ui_ReviewMessageBox.h"
+#include <QPushButton>
+
ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon)
: QDialog(parent), ui(new Ui::ReviewMessageBox)
{
ui->setupUi(this);
+ auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ back_button->setText(tr("Back"));
+
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
}
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index fcc43add..a4f4dfb9 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -96,7 +96,7 @@ AccountListPage::AccountListPage(QWidget *parent)
updateButtonStates();
// Xbox authentication won't work without a client identifier, so disable the button if it is missing
- if (~APPLICATION->currentCapabilities() & Application::SupportsMSA) {
+ if (~APPLICATION->capabilities() & Application::SupportsMSA) {
ui->actionAddMicrosoft->setVisible(false);
ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set."));
}
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index e3ac7e7c..cc597fe0 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -122,6 +122,16 @@ void MinecraftPage::loadSettings()
ui->perfomanceGroupBox->setVisible(false);
#endif
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) {
+ ui->enableFeralGamemodeCheck->setDisabled(true);
+ ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) {
+ ui->enableMangoHud->setDisabled(true);
+ ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
+
ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index 69c20309..f31e8325 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -3,109 +3,22 @@
#include "DesktopServices.h"
#include "Version.h"
-#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
#include "ui/GuiUtil.h"
#include <QKeyEvent>
#include <QMenu>
-namespace {
-// FIXME: wasteful
-void RemoveThePrefix(QString& string)
-{
- QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
- string.remove(regex);
- string = string.trimmed();
-}
-} // namespace
-
-class SortProxy : public QSortFilterProxyModel {
- public:
- explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
-
- protected:
- bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
- {
- ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
- if (!model)
- return false;
-
- const auto& mod = model->at(source_row);
-
- if (filterRegularExpression().match(mod.name()).hasMatch())
- return true;
- if (filterRegularExpression().match(mod.description()).hasMatch())
- return true;
-
- for (auto& author : mod.authors()) {
- if (filterRegularExpression().match(author).hasMatch()) {
- return true;
- }
- }
-
- return false;
- }
-
- bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
- {
- ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
- if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
- return QSortFilterProxyModel::lessThan(source_left, source_right);
- }
-
- // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
- // proceed.
-
- auto column = (ModFolderModel::Columns) source_left.column();
- bool invert = false;
- switch (column) {
- // GH-2550 - sort by enabled/disabled
- case ModFolderModel::ActiveColumn: {
- auto dataL = source_left.data(Qt::CheckStateRole).toBool();
- auto dataR = source_right.data(Qt::CheckStateRole).toBool();
- if (dataL != dataR)
- return dataL > dataR;
-
- // fallthrough
- invert = sortOrder() == Qt::DescendingOrder;
- }
- // GH-2722 - sort mod names in a way that discards "The" prefixes
- case ModFolderModel::NameColumn: {
- auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
- RemoveThePrefix(dataL);
- auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
- RemoveThePrefix(dataR);
-
- auto less = dataL.compare(dataR, sortCaseSensitivity());
- if (less != 0)
- return invert ? (less > 0) : (less < 0);
-
- // fallthrough
- invert = sortOrder() == Qt::DescendingOrder;
- }
- // GH-2762 - sort versions by parsing them as versions
- case ModFolderModel::VersionColumn: {
- auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
- auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
- return invert ? (dataL > dataR) : (dataL < dataR);
- }
- default: {
- return QSortFilterProxyModel::lessThan(source_left, source_right);
- }
- }
- }
-};
-
-ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent)
+ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
{
ui->setupUi(this);
- runningStateChanged(m_instance && m_instance->isRunning());
+ ExternalResourcesPage::runningStateChanged(m_instance && m_instance->isRunning());
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
- m_filterModel = new SortProxy(this);
+ m_filterModel = model->createFilterProxyModel(this);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
@@ -137,19 +50,9 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
ExternalResourcesPage::~ExternalResourcesPage()
{
- m_model->stopWatching();
delete ui;
}
-void ExternalResourcesPage::itemActivated(const QModelIndex&)
-{
- if (!m_controlsEnabled)
- return;
-
- auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle);
-}
-
QMenu* ExternalResourcesPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
@@ -179,6 +82,15 @@ void ExternalResourcesPage::retranslate()
ui->retranslateUi(this);
}
+void ExternalResourcesPage::itemActivated(const QModelIndex&)
+{
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
+}
+
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
{
m_viewFilter = newContents;
@@ -241,7 +153,7 @@ void ExternalResourcesPage::addItem()
if (!list.isEmpty()) {
for (auto filename : list) {
- m_model->installMod(filename);
+ m_model->installResource(filename);
}
}
}
@@ -252,25 +164,25 @@ void ExternalResourcesPage::removeItem()
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->deleteMods(selection.indexes());
+ m_model->deleteResources(selection.indexes());
}
void ExternalResourcesPage::enableItem()
{
if (!m_controlsEnabled)
return;
-
+
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setModStatus(selection.indexes(), ModFolderModel::Enable);
+ m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
}
void ExternalResourcesPage::disableItem()
{
if (!m_controlsEnabled)
return;
-
+
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setModStatus(selection.indexes(), ModFolderModel::Disable);
+ m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
}
void ExternalResourcesPage::viewConfigs()
@@ -283,15 +195,23 @@ void ExternalResourcesPage::viewFolder()
DesktopServices::openDirectory(m_model->dir().absolutePath(), true);
}
-void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
+bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
{
if (!current.isValid()) {
ui->frame->clear();
- return;
+ return false;
}
+ return onSelectionChanged(current, previous);
+}
+
+bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
+{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
- Mod& m = m_model->operator[](row);
- ui->frame->updateWithMod(m);
+ Resource const& resource = m_model->at(row);
+ ui->frame->updateWithResource(resource);
+
+ return true;
}
+
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h
index 41237139..8e352cef 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.h
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.h
@@ -7,7 +7,7 @@
#include "minecraft/MinecraftInstance.h"
#include "ui/pages/BasePage.h"
-class ModFolderModel;
+class ResourceFolderModel;
namespace Ui {
class ExternalResourcesPage;
@@ -19,8 +19,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
Q_OBJECT
public:
- // FIXME: Switch to different model (or change the name of this one)
- explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
+ explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent = nullptr);
virtual ~ExternalResourcesPage();
virtual QString displayName() const override = 0;
@@ -41,12 +40,14 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
QMenu* createPopupMenu() override;
public slots:
- void current(const QModelIndex& current, const QModelIndex& previous);
+ bool current(const QModelIndex& current, const QModelIndex& previous);
+
+ virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous);
protected slots:
void itemActivated(const QModelIndex& index);
void filterTextChanged(const QString& newContents);
- void runningStateChanged(bool running);
+ virtual void runningStateChanged(bool running);
virtual void addItem();
virtual void removeItem();
@@ -63,7 +64,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
BaseInstance* m_instance = nullptr;
Ui::ExternalResourcesPage* ui = nullptr;
- std::shared_ptr<ModFolderModel> m_model;
+ std::shared_ptr<ResourceFolderModel> m_model;
QSortFilterProxyModel* m_filterModel = nullptr;
QString m_fileSelectionFilter;
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index a13666b2..76f8ec18 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.ui
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui
@@ -43,7 +43,7 @@
</layout>
</item>
<item row="2" column="1" colspan="3">
- <widget class="MCModInfoFrame" name="frame">
+ <widget class="InfoFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
@@ -166,9 +166,9 @@
<header>ui/widgets/ModListView.h</header>
</customwidget>
<customwidget>
- <class>MCModInfoFrame</class>
+ <class>InfoFrame</class>
<extends>QFrame</extends>
- <header>ui/widgets/MCModInfoFrame.h</header>
+ <header>ui/widgets/InfoFrame.h</header>
<container>1</container>
</customwidget>
<customwidget>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index f11cf992..03910745 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -348,9 +348,19 @@ void InstanceSettingsPage::loadSettings()
ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool());
ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
- #if !defined(Q_OS_LINUX)
+#if !defined(Q_OS_LINUX)
ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false);
- #endif
+#endif
+
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) {
+ ui->enableFeralGamemodeCheck->setDisabled(true);
+ ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) {
+ ui->enableMangoHud->setDisabled(true);
+ ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
// Miscellanous
ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool());
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 14e1f1e5..28a874c2 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -65,7 +65,7 @@
#include "ui/dialogs/ProgressDialog.h"
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
- : ExternalResourcesPage(inst, mods, parent)
+ : ExternalResourcesPage(inst, mods, parent), m_model(mods)
{
// This is structured like that so that these changes
// do not affect the Resource pack and Shader pack tabs
@@ -84,49 +84,65 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
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()); });
+ auto check_allow_update = [this] {
+ return (!m_instance || !m_instance->isRunning()) &&
+ (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());
+ connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
+ });
+
+ connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
+ });
+
+ connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
+ });
+
+ connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
// Prevent a weird crash when trying to open the mods page twice in a session o.O
disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
});
+
+ ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
}
}
-CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
- : ModFolderPage(inst, mods, parent)
-{}
+void ModFolderPage::runningStateChanged(bool running)
+{
+ ExternalResourcesPage::runningStateChanged(running);
+ ui->actionDownloadItem->setEnabled(!running);
+ ui->actionUpdateItem->setEnabled(!running);
+}
bool ModFolderPage::shouldDisplay() const
{
return true;
}
-bool CoreModFolderPage::shouldDisplay() const
+bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{
- if (ModFolderPage::shouldDisplay()) {
- auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
- if (!inst)
- return true;
+ auto sourceCurrent = m_filterModel->mapToSource(current);
+ int row = sourceCurrent.row();
+ Mod const* m = m_model->at(row);
+ if (m)
+ ui->frame->updateWithMod(*m);
- auto version = inst->getPackProfile();
+ return true;
+}
- if (!version)
- return true;
- if (!version->getComponent("net.minecraftforge"))
- return false;
- if (!version->getComponent("net.minecraft"))
- return false;
- if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
- return true;
- }
- return false;
+void ModFolderPage::removeItem()
+{
+
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->deleteMods(selection.indexes());
}
void ModFolderPage::installMods()
@@ -232,3 +248,28 @@ void ModFolderPage::updateMods()
m_model->update();
}
}
+
+CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
+ : ModFolderPage(inst, mods, parent)
+{}
+
+bool CoreModFolderPage::shouldDisplay() const
+{
+ if (ModFolderPage::shouldDisplay()) {
+ auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
+ if (!inst)
+ return true;
+
+ auto version = inst->getPackProfile();
+
+ if (!version)
+ return true;
+ if (!version->getComponent("net.minecraftforge"))
+ return false;
+ if (!version->getComponent("net.minecraft"))
+ return false;
+ if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
+ return true;
+ }
+ return false;
+}
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 0a7fc9fa..c9a55bde 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -53,15 +53,30 @@ class ModFolderPage : public ExternalResourcesPage {
virtual QString helpPage() const override { return "Loader-mods"; }
virtual bool shouldDisplay() const override;
+ void runningStateChanged(bool running) override;
+
+ public slots:
+ bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
private slots:
+ void removeItem() override;
+
void installMods();
void updateMods();
+
+ protected:
+ std::shared_ptr<ModFolderModel> m_model;
};
class CoreModFolderPage : public ModFolderPage {
public:
explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0);
virtual ~CoreModFolderPage() = default;
- virtual bool shouldDisplay() const;
+
+ virtual QString displayName() const override { return tr("Core mods"); }
+ virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); }
+ virtual QString id() const override { return "coremods"; }
+ virtual QString helpPage() const override { return "Core-mods"; }
+
+ virtual bool shouldDisplay() const override;
};
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index a6c9fdd3..9633e3b4 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
+#include "minecraft/mod/ResourcePackFolderModel.h"
+
class ResourcePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
- explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ExternalResourcesPage(instance, instance->resourcePackList(), parent)
+ explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0)
+ : ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
@@ -59,4 +61,15 @@ public:
return !m_instance->traits().contains("no-texturepacks") &&
!m_instance->traits().contains("texturepacks");
}
+
+ public slots:
+ bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override
+ {
+ auto sourceCurrent = m_filterModel->mapToSource(current);
+ int row = sourceCurrent.row();
+ auto& rp = static_cast<ResourcePack&>(m_model->at(row));
+ ui->frame->updateWithResourcePack(rp);
+
+ return true;
+ }
};
diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h
index 2cc056c8..7f7ff8c1 100644
--- a/launcher/ui/pages/instance/ShaderPackPage.h
+++ b/launcher/ui/pages/instance/ShaderPackPage.h
@@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
+#include "minecraft/mod/ShaderPackFolderModel.h"
+
class ShaderPackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
- explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ExternalResourcesPage(instance, instance->shaderPackList(), parent)
+ explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0)
+ : ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h
index f550a5bc..fa219eda 100644
--- a/launcher/ui/pages/instance/TexturePackPage.h
+++ b/launcher/ui/pages/instance/TexturePackPage.h
@@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
+#include "minecraft/mod/TexturePackFolderModel.h"
+
class TexturePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
- explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ExternalResourcesPage(instance, instance->texturePackList(), parent)
+ explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
+ : ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 468ff35c..a021c633 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -196,10 +196,10 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
switch(severity)
{
case ProblemSeverity::Warning:
- ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName()));
+ ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
break;
case ProblemSeverity::Error:
- ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
+ ui->frame->setName(tr("%1 has issues!").arg(patch->getName()));
break;
default:
case ProblemSeverity::None:
@@ -222,7 +222,7 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
problemOut += problem.m_description;
problemOut += "\n";
}
- ui->frame->setModDescription(problemOut);
+ ui->frame->setDescription(problemOut);
}
void VersionPage::updateRunningStatus(bool running)
diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui
index 489f7218..fcba5598 100644
--- a/launcher/ui/pages/instance/VersionPage.ui
+++ b/launcher/ui/pages/instance/VersionPage.ui
@@ -64,7 +64,7 @@
</layout>
</item>
<item>
- <widget class="MCModInfoFrame" name="frame">
+ <widget class="InfoFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
@@ -278,9 +278,9 @@
<header>ui/widgets/ModListView.h</header>
</customwidget>
<customwidget>
- <class>MCModInfoFrame</class>
+ <class>InfoFrame</class>
<extends>QFrame</extends>
- <header>ui/widgets/MCModInfoFrame.h</header>
+ <header>ui/widgets/InfoFrame.h</header>
<container>1</container>
</customwidget>
<customwidget>
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index 647b04a7..85cc01ff 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -211,7 +211,7 @@ void WorldListPage::on_actionDatapacks_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Open World Datapacks Folder")))
return;
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
@@ -269,7 +269,7 @@ void WorldListPage::on_actionMCEdit_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Open World in MCEdit")))
return;
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
@@ -373,11 +373,11 @@ bool WorldListPage::isWorldSafe(QModelIndex)
return !m_inst->isRunning();
}
-bool WorldListPage::worldSafetyNagQuestion()
+bool WorldListPage::worldSafetyNagQuestion(const QString &actionType)
{
if(!isWorldSafe(getSelectedWorld()))
{
- auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
+ auto result = QMessageBox::question(this, actionType, tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
if(result == QMessageBox::No)
{
return false;
@@ -395,7 +395,7 @@ void WorldListPage::on_actionCopy_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Copy World")))
return;
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
@@ -417,7 +417,7 @@ void WorldListPage::on_actionRename_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Rename World")))
return;
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h
index 17e36a08..1dc9e53e 100644
--- a/launcher/ui/pages/instance/WorldListPage.h
+++ b/launcher/ui/pages/instance/WorldListPage.h
@@ -93,7 +93,7 @@ protected:
private:
QModelIndex getSelectedWorld();
bool isWorldSafe(QModelIndex index);
- bool worldSafetyNagQuestion();
+ bool worldSafetyNagQuestion(const QString &actionType);
void mceditError();
private:
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index 94b1f099..029e2be0 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -2,15 +2,27 @@
#include "BuildConfig.h"
#include "Json.h"
+#include "ModPage.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/widgets/ProjectItem.h"
+
#include <QMessageBox>
namespace ModPlatform {
-ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {}
+// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted.
+// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better?
+static QHash<ListModel*, bool> s_running;
+
+ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); }
+
+ListModel::~ListModel()
+{
+ s_running.find(this).value() = false;
+}
auto ListModel::debugName() const -> QString
{
@@ -39,9 +51,6 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
ModPlatform::IndexedPack pack = modpacks.at(pos);
switch (role) {
- case Qt::DisplayRole: {
- return pack.name;
- }
case Qt::ToolTipRole: {
if (pack.description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks
@@ -64,20 +73,20 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
}
+ case Qt::SizeHintRole:
+ return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
- case Qt::FontRole: {
- QFont font;
- if (m_parent->getDialog()->isModSelected(pack.name)) {
- font.setBold(true);
- font.setUnderline(true);
- }
-
- return font;
- }
+ // Custom data
+ case UserDataTypes::TITLE:
+ return pack.name;
+ case UserDataTypes::DESCRIPTION:
+ return pack.description;
+ case UserDataTypes::SELECTED:
+ return m_parent->getDialog()->isModSelected(pack.name);
default:
break;
}
@@ -85,11 +94,27 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
return {};
}
-void ListModel::requestModVersions(ModPlatform::IndexedPack const& current)
+bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ int pos = index.row();
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid())
+ return false;
+
+ modpacks[pos] = value.value<ModPlatform::IndexedPack>();
+
+ return true;
+}
+
+void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
- m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() });
+ m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() },
+ [this, current, index](QJsonDocument& doc, QString addonId) {
+ if (!s_running.constFind(this).value())
+ return;
+ versionRequestSucceeded(doc, addonId, index);
+ });
}
void ListModel::performPaginatedSearch()
@@ -100,9 +125,13 @@ void ListModel::performPaginatedSearch()
this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
}
-void ListModel::requestModInfo(ModPlatform::IndexedPack& current)
+void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
{
- m_parent->apiProvider()->getModInfo(this, current);
+ m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
+ if (!s_running.constFind(this).value())
+ return;
+ infoRequestFinished(doc, pack, index);
+ });
}
void ListModel::refresh()
@@ -230,10 +259,11 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
void ListModel::searchRequestFailed(QString reason)
{
- if (!jobPtr->first()->m_reply) {
+ auto failed_action = jobPtr->getFailedActions().at(0);
+ if (!failed_action->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
- } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
+ } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
@@ -255,7 +285,7 @@ void ListModel::searchRequestFailed(QString reason)
}
}
-void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack)
+void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
qDebug() << "Loading mod info";
@@ -267,10 +297,20 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack
qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause();
}
+ // Check if the index is still valid for this mod or not
+ if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) {
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache mod info!";
+ }
+ }
+
m_parent->updateUi();
}
-void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
+void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
{
auto& current = m_parent->getCurrent();
if (addonId != current.addonId) {
@@ -286,6 +326,14 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
}
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache mod versions!";
+ }
+
+
m_parent->updateModVersions();
}
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index dd22407c..a58c7c55 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -2,7 +2,6 @@
#include <QAbstractListModel>
-#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
#include "net/NetJob.h"
@@ -19,7 +18,7 @@ class ListModel : public QAbstractListModel {
public:
ListModel(ModPage* parent);
- ~ListModel() override = default;
+ ~ListModel() override;
inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; };
@@ -29,15 +28,17 @@ class ListModel : public QAbstractListModel {
/* Retrieve information from the model at a given index with the given role */
auto data(const QModelIndex& index, int role) const -> QVariant override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
+ inline NetJob* activeJob() { return jobPtr.get(); }
/* Ask the API for more information */
void fetchMore(const QModelIndex& parent) override;
void refresh();
void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
- void requestModInfo(ModPlatform::IndexedPack& current);
- void requestModVersions(const ModPlatform::IndexedPack& current);
+ void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index);
+ void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {};
@@ -51,9 +52,9 @@ class ListModel : public QAbstractListModel {
void searchRequestFinished(QJsonDocument& doc);
void searchRequestFailed(QString reason);
- void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack);
+ void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index);
- void versionRequestSucceeded(QJsonDocument doc, QString addonId);
+ void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index);
protected slots:
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 200fe59e..986caa77 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -40,37 +40,45 @@
#include <QKeyEvent>
#include <memory>
+#include <HoeDown.h>
+
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/widgets/ProjectItem.h"
+
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
: QWidget(dialog)
, m_instance(instance)
, ui(new Ui::ModPage)
, dialog(dialog)
- , filter_widget(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this)
+ , m_fetch_progress(this, false)
, api(api)
{
ui->setupUi(this);
+
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
+
+ m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
+ m_search_timer.setSingleShot(true);
+
+ connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch);
+
ui->searchEdit->installEventFilter(this);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
- ui->gridLayout_3->addWidget(&filter_widget, 0, 0, 1, ui->gridLayout_3->columnCount());
+ m_fetch_progress.hideIfInactive(true);
+ m_fetch_progress.setFixedHeight(24);
+ m_fetch_progress.progressFormat("");
- filter_widget.setInstance(static_cast<MinecraftInstance*>(m_instance));
- m_filter = filter_widget.getFilter();
+ ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount());
- connect(&filter_widget, &ModFilterWidget::filterChanged, this, [&]{
- ui->searchButton->setStyleSheet("text-decoration: underline");
- });
- connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{
- ui->searchButton->setStyleSheet("text-decoration: none");
- });
+ ui->packView->setItemDelegate(new ProjectItemDelegate(this));
+ ui->packView->installEventFilter(this);
}
ModPage::~ModPage()
@@ -78,6 +86,26 @@ ModPage::~ModPage()
delete ui;
}
+void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
+{
+ if (m_filter_widget)
+ disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr);
+
+ m_filter_widget.swap(widget);
+
+ ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount());
+
+ m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance));
+ m_filter = m_filter_widget->getFilter();
+
+ connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
+ ui->searchButton->setStyleSheet("text-decoration: underline");
+ });
+ connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
+ ui->searchButton->setStyleSheet("text-decoration: none");
+ });
+}
+
/******** Qt things ********/
@@ -95,6 +123,23 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
triggerSearch();
keyEvent->accept();
return true;
+ } else {
+ if (m_search_timer.isActive())
+ m_search_timer.stop();
+
+ m_search_timer.start(350);
+ }
+ } else if (watched == ui->packView && event->type() == QEvent::KeyPress) {
+ auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ onModSelected();
+
+ // To have the 'select mod' button outlined instead of the 'review and confirm' one
+ ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
+ ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
+
+ keyEvent->accept();
+ return true;
}
}
return QWidget::eventFilter(watched, event);
@@ -105,13 +150,13 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
void ModPage::filterMods()
{
- filter_widget.setHidden(!filter_widget.isHidden());
+ m_filter_widget->setHidden(!m_filter_widget->isHidden());
}
void ModPage::triggerSearch()
{
- auto changed = filter_widget.changed();
- m_filter = filter_widget.getFilter();
+ auto changed = m_filter_widget->changed();
+ m_filter = m_filter_widget->getFilter();
if(changed){
ui->packView->clearSelection();
@@ -120,16 +165,26 @@ void ModPage::triggerSearch()
updateSelectionButton();
}
- listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), changed);
+ listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
+ m_fetch_progress.watch(listModel->activeJob());
}
-void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+QString ModPage::getSearchTerm() const
+{
+ return ui->searchEdit->text();
+}
+void ModPage::setSearchTerm(QString term)
+{
+ ui->searchEdit->setText(term);
+}
+
+void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
ui->versionSelectionBox->clear();
- if (!first.isValid()) { return; }
+ if (!curr.isValid()) { return; }
- current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>();
+ current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (!current.versionsLoaded) {
qDebug() << QString("Loading %1 mod versions").arg(debugName());
@@ -137,7 +192,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
ui->modSelectionButton->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false);
- listModel->requestModVersions(current);
+ listModel->requestModVersions(current, curr);
} else {
for (int i = 0; i < current.versions.size(); i++) {
ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
@@ -149,7 +204,8 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
if(!current.extraDataLoaded){
qDebug() << QString("Loading %1 mod info").arg(debugName());
- listModel->requestModInfo(current);
+
+ listModel->requestModInfo(current, curr);
}
updateUi();
@@ -167,6 +223,9 @@ void ModPage::onVersionSelectionChanged(QString data)
void ModPage::onModSelected()
{
+ if (selectedVersion < 0)
+ return;
+
auto& version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
@@ -176,6 +235,9 @@ void ModPage::onModSelected()
}
updateSelectionButton();
+
+ /* Force redraw on the mods list when the selection changes */
+ ui->packView->adjustSize();
}
@@ -285,5 +347,6 @@ void ModPage::updateUi()
text += "<hr>";
- ui->packDescription->setHtml(text + current.description);
+ HoeDown h;
+ ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8())));
}
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index cf00e16e..3f31651c 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -8,6 +8,7 @@
#include "ui/pages/BasePage.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/widgets/ModFilterWidget.h"
+#include "ui/widgets/ProgressWidget.h"
class ModDownloadDialog;
@@ -20,7 +21,17 @@ class ModPage : public QWidget, public BasePage {
Q_OBJECT
public:
- explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
+ template<typename T>
+ static T* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ {
+ auto page = new T(dialog, instance);
+
+ auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page);
+ page->setFilterWidget(filter_widget);
+
+ return page;
+ }
+
~ModPage() override;
/* Affects what the user sees */
@@ -45,6 +56,13 @@ class ModPage : public QWidget, public BasePage {
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
auto getDialog() const -> const ModDownloadDialog* { return dialog; }
+ /** Get the current term in the search bar. */
+ auto getSearchTerm() const -> QString;
+ /** Programatically set the term in the search bar. */
+ void setSearchTerm(QString);
+
+ void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
+
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
void updateModVersions(int prev_count = -1);
@@ -54,6 +72,7 @@ class ModPage : public QWidget, public BasePage {
BaseInstance* m_instance;
protected:
+ ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
void updateSelectionButton();
protected slots:
@@ -67,13 +86,18 @@ class ModPage : public QWidget, public BasePage {
Ui::ModPage* ui = nullptr;
ModDownloadDialog* dialog = nullptr;
- ModFilterWidget filter_widget;
+ unique_qobject_ptr<ModFilterWidget> m_filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
+ ProgressWidget m_fetch_progress;
+
ModPlatform::ListModel* listModel = nullptr;
ModPlatform::IndexedPack current;
std::unique_ptr<ModAPI> api;
int selectedVersion = -1;
+
+ // Used to do instant searching with a delay to cache quick changes
+ QTimer m_search_timer;
};
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
index 8de5211c..7901b90b 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
@@ -37,13 +37,12 @@
#include "AtlPage.h"
#include "ui_AtlPage.h"
-#include "modplatform/atlauncher/ATLPackInstallTask.h"
+#include "BuildConfig.h"
#include "AtlOptionalModDialog.h"
+#include "AtlUserInteractionSupportImpl.h"
+#include "modplatform/atlauncher/ATLPackInstallTask.h"
#include "ui/dialogs/NewInstanceDialog.h"
-#include "ui/dialogs/VersionSelectDialog.h"
-
-#include <BuildConfig.h>
#include <QMessageBox>
@@ -117,7 +116,9 @@ void AtlPage::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion));
+ auto uiSupport = new AtlUserInteractionSupportImpl(this);
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion));
+
auto editedLogoName = selected.safeName;
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo)
@@ -172,51 +173,3 @@ void AtlPage::onVersionSelectionChanged(QString data)
selectedVersion = data;
suggestCurrent();
}
-
-QVector<QString> AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
-{
- AtlOptionalModDialog optionalModDialog(this, version, mods);
- optionalModDialog.exec();
- return optionalModDialog.getResult();
-}
-
-QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) {
- VersionSelectDialog vselect(vlist.get(), "Choose Version", APPLICATION->activeWindow(), false);
- if (minecraftVersion != Q_NULLPTR) {
- vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
- vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
- }
- else {
- vselect.setEmptyString(tr("No versions are currently available"));
- }
- vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
-
- // select recommended build
- for (int i = 0; i < vlist->versions().size(); i++) {
- auto version = vlist->versions().at(i);
- auto reqs = version->requires();
-
- // filter by minecraft version, if the loader depends on a certain version.
- if (minecraftVersion != Q_NULLPTR) {
- auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
- return req.uid == "net.minecraft";
- });
- if (iter == reqs.end()) continue;
- if (iter->equalsVersion != minecraftVersion) continue;
- }
-
- // first recommended build we find, we use.
- if (version->isRecommended()) {
- vselect.setCurrentVersion(version->descriptor());
- break;
- }
- }
-
- vselect.exec();
- return vselect.selectedVersion()->descriptor();
-}
-
-void AtlPage::displayMessage(QString message)
-{
- QMessageBox::information(this, tr("Installing"), message);
-}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
index aa6d5da1..1b3b15c1 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
@@ -52,7 +52,7 @@ namespace Ui
class NewInstanceDialog;
-class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport
+class AtlPage : public QWidget, public BasePage
{
Q_OBJECT
@@ -83,10 +83,6 @@ public:
private:
void suggestCurrent();
- QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
- QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
- void displayMessage(QString message) override;
-
private slots:
void triggerSearch();
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
new file mode 100644
index 00000000..03196685
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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 <QMessageBox>
+#include "AtlUserInteractionSupportImpl.h"
+
+#include "AtlOptionalModDialog.h"
+#include "ui/dialogs/VersionSelectDialog.h"
+
+AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget *parent) : m_parent(parent)
+{
+}
+
+QVector<QString> AtlUserInteractionSupportImpl::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
+{
+ AtlOptionalModDialog optionalModDialog(m_parent, version, mods);
+ optionalModDialog.exec();
+ return optionalModDialog.getResult();
+}
+
+QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion)
+{
+ VersionSelectDialog vselect(vlist.get(), "Choose Version", m_parent, false);
+ if (minecraftVersion != nullptr) {
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
+ vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
+ }
+ else {
+ vselect.setEmptyString(tr("No versions are currently available"));
+ }
+ vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
+
+ // select recommended build
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ if (minecraftVersion != nullptr) {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end())
+ continue;
+ if (iter->equalsVersion != minecraftVersion)
+ continue;
+ }
+
+ // first recommended build we find, we use.
+ if (version->isRecommended()) {
+ vselect.setCurrentVersion(version->descriptor());
+ break;
+ }
+ }
+
+ vselect.exec();
+ return vselect.selectedVersion()->descriptor();
+}
+
+void AtlUserInteractionSupportImpl::displayMessage(QString message)
+{
+ QMessageBox::information(m_parent, tr("Installing"), message);
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
new file mode 100644
index 00000000..aa22fc73
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+
+#include "modplatform/atlauncher/ATLPackInstallTask.h"
+
+class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport {
+ Q_OBJECT
+
+public:
+ AtlUserInteractionSupportImpl(QWidget* parent);
+
+private:
+ QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
+ QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
+ void displayMessage(QString message) override;
+
+private:
+ QWidget* m_parent;
+
+};
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
index 8de2e545..bc2c686c 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
@@ -12,6 +12,12 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
FlameMod::loadIndexedPack(m, obj);
}
+// We already deal with the URLs when initializing the pack, due to the API response's structure
+void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadBody(m, obj);
+}
+
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h
index 707c1bb1..6a6aef2e 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h
@@ -13,6 +13,7 @@ class ListModel : public ModPlatform::ListModel {
private:
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h
index 445d0368..2cd484cb 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h
@@ -44,7 +44,12 @@ class FlameModPage : public ModPage {
Q_OBJECT
public:
- explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ {
+ return ModPage::create<FlameModPage>(dialog, instance);
+ }
+
+ FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~FlameModPage() override = default;
inline auto displayName() const -> QString override { return "CurseForge"; }
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/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
index f4231d8d..ad08dc25 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
@@ -35,7 +35,11 @@
</widget>
</item>
<item row="0" column="1">
- <widget class="QTextBrowser" name="publicPackDescription"/>
+ <widget class="QTextBrowser" name="publicPackDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
</layout>
</widget>
@@ -45,7 +49,11 @@
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
- <widget class="QTextBrowser" name="thirdPartyPackDescription"/>
+ <widget class="QTextBrowser" name="thirdPartyPackDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
<item row="0" column="0">
<widget class="QTreeView" name="thirdPartyPackList">
@@ -95,7 +103,11 @@
</widget>
</item>
<item row="0" column="1" rowspan="3">
- <widget class="QTextBrowser" name="privatePackDescription"/>
+ <widget class="QTextBrowser" name="privatePackDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
</layout>
</widget>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
index 94985f63..40d82e6f 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
@@ -44,7 +44,12 @@ class ModrinthModPage : public ModPage {
Q_OBJECT
public:
- explicit ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ {
+ return ModPage::create<ModrinthModPage>(dialog, instance);
+ }
+
+ ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~ModrinthModPage() override = default;
inline auto displayName() const -> QString override { return "Modrinth"; }
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 3633d575..614be434 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -301,10 +301,11 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
void ModpackListModel::searchRequestFailed(QString reason)
{
- if (!jobPtr->first()->m_reply) {
+ auto failed_action = jobPtr->getFailedActions().at(0);
+ if (!failed_action->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks."));
- } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
+ } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
diff --git a/launcher/ui/widgets/Common.cpp b/launcher/ui/widgets/Common.cpp
index f72f3596..097bb6d4 100644
--- a/launcher/ui/widgets/Common.cpp
+++ b/launcher/ui/widgets/Common.cpp
@@ -1,27 +1,33 @@
#include "Common.h"
// Origin: Qt
-QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
- qreal &widthUsed)
+// More specifically, this is a trimmed down version on the algorithm in:
+// https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846
+QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height)
{
- QStringList lines;
+ QList<std::pair<qreal, QString>> lines;
height = 0;
- widthUsed = 0;
+
textLayout.beginLayout();
+
QString str = textLayout.text();
- while (true)
- {
+ while (true) {
QTextLine line = textLayout.createLine();
+
if (!line.isValid())
break;
if (line.textLength() == 0)
break;
+
line.setLineWidth(lineWidth);
line.setPosition(QPointF(0, height));
+
height += line.height();
- lines.append(str.mid(line.textStart(), line.textLength()));
- widthUsed = qMax(widthUsed, line.naturalTextWidth());
+
+ lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength())));
}
+
textLayout.endLayout();
+
return lines;
}
diff --git a/launcher/ui/widgets/Common.h b/launcher/ui/widgets/Common.h
index b3fbe1a0..b3dd5ca8 100644
--- a/launcher/ui/widgets/Common.h
+++ b/launcher/ui/widgets/Common.h
@@ -1,6 +1,9 @@
#pragma once
-#include <QStringList>
+
#include <QTextLayout>
-QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
- qreal &widthUsed); \ No newline at end of file
+/** Cuts out the text in textLayout into smaller pieces, according to the lineWidth.
+ * Returns a list of pairs, each containing the width of that line and that line's string, respectively.
+ * The total height of those lines is set in the last argument, 'height'.
+ */
+QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height);
diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp
new file mode 100644
index 00000000..9e0553f8
--- /dev/null
+++ b/launcher/ui/widgets/InfoFrame.cpp
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+* PolyMC - Minecraft Launcher
+* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, version 3.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see <https://www.gnu.org/licenses/>.
+*
+* This file incorporates work covered by the following copyright and
+* permission notice:
+*
+* Copyright 2013-2021 MultiMC Contributors
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#include <QMessageBox>
+
+#include "InfoFrame.h"
+#include "ui_InfoFrame.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+InfoFrame::InfoFrame(QWidget *parent) :
+ QFrame(parent),
+ ui(new Ui::InfoFrame)
+{
+ ui->setupUi(this);
+ ui->descriptionLabel->setHidden(true);
+ ui->nameLabel->setHidden(true);
+ updateHiddenState();
+}
+
+InfoFrame::~InfoFrame()
+{
+ delete ui;
+}
+
+void InfoFrame::updateWithMod(Mod const& m)
+{
+ if (m.type() == ResourceType::FOLDER)
+ {
+ clear();
+ return;
+ }
+
+ QString text = "";
+ QString name = "";
+ if (m.name().isEmpty())
+ name = m.internal_id();
+ else
+ name = m.name();
+
+ if (m.homeurl().isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
+ if (!m.authors().isEmpty())
+ text += " by " + m.authors().join(", ");
+
+ setName(text);
+
+ if (m.description().isEmpty())
+ {
+ setDescription(QString());
+ }
+ else
+ {
+ setDescription(m.description());
+ }
+
+ setImage();
+}
+
+void InfoFrame::updateWithResource(const Resource& resource)
+{
+ setName(resource.name());
+ setImage();
+}
+
+// https://www.sportskeeda.com/minecraft-wiki/color-codes
+static const QMap<QChar, QString> s_value_to_color = {
+ {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"},
+ {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"},
+ {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"},
+ {'f', "#FFFFFF"}
+};
+
+void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
+{
+ setName(resource_pack.name());
+
+ // We have to manually set the colors for use.
+ //
+ // A color is set using §x, with x = a hex number from 0 to f.
+ //
+ // We traverse the description and, when one of those is found, we create
+ // a span element with that color set.
+ //
+ // TODO: Make the same logic for font formatting too.
+ // TODO: Wrap links inside <a> tags
+
+ auto description = resource_pack.description();
+
+ QString description_parsed("<html>");
+ bool in_div = false;
+
+ auto desc_it = description.constBegin();
+ while (desc_it != description.constEnd()) {
+ if (*desc_it == u'§') {
+ if (in_div)
+ description_parsed += "</span>";
+
+ auto const& num = *(++desc_it);
+ description_parsed += QString("<span style=\"color: %1;\">").arg(s_value_to_color.constFind(num).value());
+
+ in_div = true;
+
+ desc_it++;
+ }
+
+ description_parsed += *desc_it;
+ desc_it++;
+ }
+
+ if (in_div)
+ description_parsed += "</span>";
+ description_parsed += "</html>";
+
+ description_parsed.replace("\n", "<br>");
+
+ setDescription(description_parsed);
+ setImage(resource_pack.image({64, 64}));
+}
+
+void InfoFrame::clear()
+{
+ setName();
+ setDescription();
+ setImage();
+}
+
+void InfoFrame::updateHiddenState()
+{
+ if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden())
+ {
+ setHidden(true);
+ }
+ else
+ {
+ setHidden(false);
+ }
+}
+
+void InfoFrame::setName(QString text)
+{
+ if(text.isEmpty())
+ {
+ ui->nameLabel->setHidden(true);
+ }
+ else
+ {
+ ui->nameLabel->setText(text);
+ ui->nameLabel->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void InfoFrame::setDescription(QString text)
+{
+ if(text.isEmpty())
+ {
+ ui->descriptionLabel->setHidden(true);
+ updateHiddenState();
+ return;
+ }
+ else
+ {
+ ui->descriptionLabel->setHidden(false);
+ updateHiddenState();
+ }
+ ui->descriptionLabel->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ foreach(const QChar& c, intermediatetext)
+ {
+ if(c == rem && prev){
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if(finaltext.length() > 290)
+ {
+ ui->descriptionLabel->setOpenExternalLinks(false);
+ ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
+ m_description = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
+ QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
+ }
+ else
+ {
+ ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
+ labeltext.append(finaltext);
+ }
+ ui->descriptionLabel->setText(labeltext);
+}
+
+void InfoFrame::setImage(QPixmap img)
+{
+ if (img.isNull()) {
+ ui->iconLabel->setHidden(true);
+ } else {
+ ui->iconLabel->setHidden(false);
+ ui->iconLabel->setPixmap(img);
+ }
+}
+
+void InfoFrame::descriptionEllipsisHandler(QString link)
+{
+ if(!m_current_box)
+ {
+ m_current_box = CustomMessageBox::selectable(this, "", m_description);
+ connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
+ m_current_box->show();
+ }
+ else
+ {
+ m_current_box->setText(m_description);
+ }
+}
+
+void InfoFrame::boxClosed(int result)
+{
+ m_current_box = nullptr;
+}
diff --git a/launcher/ui/widgets/MCModInfoFrame.h b/launcher/ui/widgets/InfoFrame.h
index 0b7ef537..70d15b1e 100644
--- a/launcher/ui/widgets/MCModInfoFrame.h
+++ b/launcher/ui/widgets/InfoFrame.h
@@ -16,37 +16,41 @@
#pragma once
#include <QFrame>
+
#include "minecraft/mod/Mod.h"
+#include "minecraft/mod/ResourcePack.h"
namespace Ui
{
-class MCModInfoFrame;
+class InfoFrame;
}
-class MCModInfoFrame : public QFrame
-{
+class InfoFrame : public QFrame {
Q_OBJECT
-public:
- explicit MCModInfoFrame(QWidget *parent = 0);
- ~MCModInfoFrame();
+ public:
+ InfoFrame(QWidget* parent = nullptr);
+ ~InfoFrame() override;
- void setModText(QString text);
- void setModDescription(QString text);
+ void setName(QString text = {});
+ void setDescription(QString text = {});
+ void setImage(QPixmap img = {});
- void updateWithMod(Mod &m);
void clear();
-public slots:
- void modDescEllipsisHandler(const QString& link );
+ void updateWithMod(Mod const& m);
+ void updateWithResource(Resource const& resource);
+ void updateWithResourcePack(ResourcePack& rp);
+
+ public slots:
+ void descriptionEllipsisHandler(QString link);
void boxClosed(int result);
-private:
+ private:
void updateHiddenState();
-private:
- Ui::MCModInfoFrame *ui;
- QString desc;
- class QMessageBox * currentBox = nullptr;
+ private:
+ Ui::InfoFrame* ui;
+ QString m_description;
+ class QMessageBox* m_current_box = nullptr;
};
-
diff --git a/launcher/ui/widgets/MCModInfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui
index 5ef33379..9e407ce9 100644
--- a/launcher/ui/widgets/MCModInfoFrame.ui
+++ b/launcher/ui/widgets/InfoFrame.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>MCModInfoFrame</class>
- <widget class="QFrame" name="MCModInfoFrame">
+ <class>InfoFrame</class>
+ <widget class="QFrame" name="InfoFrame">
<property name="geometry">
<rect>
<x>0</x>
@@ -22,10 +22,7 @@
<height>120</height>
</size>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <property name="spacing">
- <number>6</number>
- </property>
+ <layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
@@ -38,8 +35,8 @@
<property name="bottomMargin">
<number>0</number>
</property>
- <item>
- <widget class="QLabel" name="label_ModText">
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
<property name="text">
<string notr="true"/>
</property>
@@ -60,8 +57,8 @@
</property>
</widget>
</item>
- <item>
- <widget class="QLabel" name="label_ModDescription">
+ <item row="1" column="1">
+ <widget class="QLabel" name="descriptionLabel">
<property name="toolTip">
<string notr="true"/>
</property>
@@ -85,6 +82,31 @@
</property>
</widget>
</item>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QLabel" name="iconLabel">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="scaledContents">
+ <bool>false</bool>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
<resources/>
diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp
deleted file mode 100644
index 7d78006b..00000000
--- a/launcher/ui/widgets/MCModInfoFrame.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <QMessageBox>
-#include <QtGui>
-
-#include "MCModInfoFrame.h"
-#include "ui_MCModInfoFrame.h"
-
-#include "ui/dialogs/CustomMessageBox.h"
-
-void MCModInfoFrame::updateWithMod(Mod &m)
-{
- if (m.type() == m.MOD_FOLDER)
- {
- clear();
- return;
- }
-
- QString text = "";
- QString name = "";
- if (m.name().isEmpty())
- name = m.internal_id();
- else
- name = m.name();
-
- if (m.homeurl().isEmpty())
- text = name;
- else
- text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
- if (!m.authors().isEmpty())
- text += " by " + m.authors().join(", ");
-
- setModText(text);
-
- if (m.description().isEmpty())
- {
- setModDescription(QString());
- }
- else
- {
- setModDescription(m.description());
- }
-}
-
-void MCModInfoFrame::clear()
-{
- setModText(QString());
- setModDescription(QString());
-}
-
-MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
- QFrame(parent),
- ui(new Ui::MCModInfoFrame)
-{
- ui->setupUi(this);
- ui->label_ModDescription->setHidden(true);
- ui->label_ModText->setHidden(true);
- updateHiddenState();
-}
-
-MCModInfoFrame::~MCModInfoFrame()
-{
- delete ui;
-}
-
-void MCModInfoFrame::updateHiddenState()
-{
- if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
- {
- setHidden(true);
- }
- else
- {
- setHidden(false);
- }
-}
-
-void MCModInfoFrame::setModText(QString text)
-{
- if(text.isEmpty())
- {
- ui->label_ModText->setHidden(true);
- }
- else
- {
- ui->label_ModText->setText(text);
- ui->label_ModText->setHidden(false);
- }
- updateHiddenState();
-}
-
-void MCModInfoFrame::setModDescription(QString text)
-{
- if(text.isEmpty())
- {
- ui->label_ModDescription->setHidden(true);
- updateHiddenState();
- return;
- }
- else
- {
- ui->label_ModDescription->setHidden(false);
- updateHiddenState();
- }
- ui->label_ModDescription->setToolTip("");
- QString intermediatetext = text.trimmed();
- bool prev(false);
- QChar rem('\n');
- QString finaltext;
- finaltext.reserve(intermediatetext.size());
- foreach(const QChar& c, intermediatetext)
- {
- if(c == rem && prev){
- continue;
- }
- prev = c == rem;
- finaltext += c;
- }
- QString labeltext;
- labeltext.reserve(300);
- if(finaltext.length() > 290)
- {
- ui->label_ModDescription->setOpenExternalLinks(false);
- ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
- desc = text;
- // This allows injecting HTML here.
- labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
- QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler);
- }
- else
- {
- ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
- labeltext.append(finaltext);
- }
- ui->label_ModDescription->setText(labeltext);
-}
-
-void MCModInfoFrame::modDescEllipsisHandler(const QString &link)
-{
- if(!currentBox)
- {
- currentBox = CustomMessageBox::selectable(this, QString(), desc);
- connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
- currentBox->show();
- }
- else
- {
- currentBox->setText(desc);
- }
-}
-
-void MCModInfoFrame::boxClosed(int result)
-{
- currentBox = nullptr;
-}
diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp
index 4ab34375..ea052c41 100644
--- a/launcher/ui/widgets/ModFilterWidget.cpp
+++ b/launcher/ui/widgets/ModFilterWidget.cpp
@@ -1,6 +1,39 @@
#include "ModFilterWidget.h"
#include "ui_ModFilterWidget.h"
+#include "Application.h"
+
+unique_qobject_ptr<ModFilterWidget> ModFilterWidget::create(Version default_version, QWidget* parent)
+{
+ auto filter_widget = new ModFilterWidget(default_version, parent);
+
+ if (!filter_widget->versionList()->isLoaded()) {
+ QEventLoop load_version_list_loop;
+
+ QTimer time_limit_for_list_load;
+ time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer);
+ time_limit_for_list_load.setSingleShot(true);
+ time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit);
+ time_limit_for_list_load.start(4000);
+
+ auto task = filter_widget->versionList()->getLoadTask();
+
+ connect(task.get(), &Task::failed, [filter_widget]{
+ filter_widget->disableVersionButton(VersionButtonID::Major, tr("failed to get version index"));
+ });
+ connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit);
+
+ if (!task->isRunning())
+ task->start();
+
+ load_version_list_loop.exec();
+ if (time_limit_for_list_load.isActive())
+ time_limit_for_list_load.stop();
+ }
+
+ return unique_qobject_ptr<ModFilterWidget>(filter_widget);
+}
+
ModFilterWidget::ModFilterWidget(Version def, QWidget* parent)
: QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget)
{
@@ -16,6 +49,7 @@ ModFilterWidget::ModFilterWidget(Version def, QWidget* parent)
m_filter->versions.push_front(def);
+ m_version_list = APPLICATION->metadataIndex()->get("net.minecraft");
setHidden(true);
}
@@ -51,24 +85,30 @@ auto ModFilterWidget::getFilter() -> std::shared_ptr<Filter>
return m_filter;
}
-void ModFilterWidget::disableVersionButton(VersionButtonID id)
+void ModFilterWidget::disableVersionButton(VersionButtonID id, QString reason)
{
+ QAbstractButton* btn = nullptr;
+
switch(id){
case(VersionButtonID::Strict):
- ui->strictVersionButton->setEnabled(false);
+ btn = ui->strictVersionButton;
break;
case(VersionButtonID::Major):
- ui->majorVersionButton->setEnabled(false);
+ btn = ui->majorVersionButton;
break;
case(VersionButtonID::All):
- ui->allVersionsButton->setEnabled(false);
+ btn = ui->allVersionsButton;
break;
case(VersionButtonID::Between):
- // ui->betweenVersionsButton->setEnabled(false);
- break;
default:
break;
}
+
+ if (btn) {
+ btn->setEnabled(false);
+ if (!reason.isEmpty())
+ btn->setText(btn->text() + QString(" (%1)").arg(reason));
+ }
}
void ModFilterWidget::onVersionFilterChanged(int id)
@@ -76,7 +116,7 @@ void ModFilterWidget::onVersionFilterChanged(int id)
//ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between);
//ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between);
- int index = 0;
+ int index = 1;
auto cast_id = (VersionButtonID) id;
if (cast_id != m_version_id) {
@@ -93,10 +133,15 @@ void ModFilterWidget::onVersionFilterChanged(int id)
break;
case(VersionButtonID::Major): {
auto versionSplit = mcVersionStr().split(".");
- for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){
- m_filter->versions.push_front(i);
- i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index)));
+
+ auto major_version = QString("%1.%2").arg(versionSplit[0], versionSplit[1]);
+ QString version_str = major_version;
+
+ while (m_version_list->hasVersion(version_str)) {
+ m_filter->versions.emplace_back(version_str);
+ version_str = QString("%1.%2").arg(major_version, QString::number(index++));
}
+
break;
}
case(VersionButtonID::All):
diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h
index 334fc672..958a1e2b 100644
--- a/launcher/ui/widgets/ModFilterWidget.h
+++ b/launcher/ui/widgets/ModFilterWidget.h
@@ -4,6 +4,10 @@
#include <QButtonGroup>
#include "Version.h"
+
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
@@ -34,18 +38,22 @@ public:
std::shared_ptr<Filter> m_filter;
public:
- explicit ModFilterWidget(Version def, QWidget* parent = nullptr);
+ static unique_qobject_ptr<ModFilterWidget> create(Version default_version, QWidget* parent = nullptr);
~ModFilterWidget();
void setInstance(MinecraftInstance* instance);
/// By default all buttons are enabled
- void disableVersionButton(VersionButtonID);
+ void disableVersionButton(VersionButtonID, QString reason = {});
auto getFilter() -> std::shared_ptr<Filter>;
auto changed() const -> bool { return m_last_version_id != m_version_id; }
+ Meta::VersionListPtr versionList() { return m_version_list; }
+
private:
+ ModFilterWidget(Version def, QWidget* parent = nullptr);
+
inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; }
inline auto mcVersion() const -> Version { return { mcVersionStr() }; }
@@ -61,8 +69,12 @@ private:
MinecraftInstance* m_instance = nullptr;
+
+/* Version stuff */
QButtonGroup m_mcVersion_buttons;
+ Meta::VersionListPtr m_version_list;
+
/* Used to tell if the filter was changed since the last getFilter() call */
VersionButtonID m_last_version_id = VersionButtonID::Strict;
VersionButtonID m_version_id = VersionButtonID::Strict;
diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp
index 419ccb66..8d606820 100644
--- a/launcher/ui/widgets/PageContainer.cpp
+++ b/launcher/ui/widgets/PageContainer.cpp
@@ -244,7 +244,14 @@ void PageContainer::help()
void PageContainer::currentChanged(const QModelIndex &current)
{
- showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1);
+ int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1;
+
+ auto* selected = m_model->pages().at(selected_index);
+ auto* previous = m_currentPage;
+
+ emit selectedPageChanged(previous, selected);
+
+ showPage(selected_index);
}
bool PageContainer::prepareToClose()
diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h
index 86f549eb..80d87a9b 100644
--- a/launcher/ui/widgets/PageContainer.h
+++ b/launcher/ui/widgets/PageContainer.h
@@ -95,6 +95,10 @@ private:
public slots:
void help();
+signals:
+ /** Emitted when the currently selected page is changed */
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
private slots:
void currentChanged(const QModelIndex &current);
void showPage(int row);
diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
index 911e555d..b60d9a7a 100644
--- a/launcher/ui/widgets/ProgressWidget.cpp
+++ b/launcher/ui/widgets/ProgressWidget.cpp
@@ -1,66 +1,104 @@
// Licensed under the Apache-2.0 license. See README.md for details.
#include "ProgressWidget.h"
-#include <QProgressBar>
+#include <QEventLoop>
#include <QLabel>
+#include <QProgressBar>
#include <QVBoxLayout>
-#include <QEventLoop>
#include "tasks/Task.h"
-ProgressWidget::ProgressWidget(QWidget *parent)
- : QWidget(parent)
+ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent)
{
- m_label = new QLabel(this);
- m_label->setWordWrap(true);
+ auto* layout = new QVBoxLayout(this);
+
+ if (show_label) {
+ m_label = new QLabel(this);
+ m_label->setWordWrap(true);
+ layout->addWidget(m_label);
+ }
+
m_bar = new QProgressBar(this);
m_bar->setMinimum(0);
m_bar->setMaximum(100);
- QVBoxLayout *layout = new QVBoxLayout(this);
- layout->addWidget(m_label);
layout->addWidget(m_bar);
- layout->addStretch();
+
setLayout(layout);
}
-void ProgressWidget::start(std::shared_ptr<Task> task)
+void ProgressWidget::reset()
+{
+ m_bar->reset();
+}
+
+void ProgressWidget::progressFormat(QString format)
+{
+ if (format.isEmpty())
+ m_bar->setTextVisible(false);
+ else
+ m_bar->setFormat(format);
+}
+
+void ProgressWidget::watch(Task* task)
{
+ if (!task)
+ return;
+
if (m_task)
- {
- disconnect(m_task.get(), 0, this, 0);
- }
+ disconnect(m_task, nullptr, this, nullptr);
+
m_task = task;
- connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish);
- connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus);
- connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress);
- connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed);
+
+ connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish);
+ connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus);
+ connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
+ connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
+
+ show();
+}
+
+void ProgressWidget::start(Task* task)
+{
+ watch(task);
if (!m_task->isRunning())
- {
- QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection);
- }
+ QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
}
+
bool ProgressWidget::exec(std::shared_ptr<Task> task)
{
QEventLoop loop;
+
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
- start(task);
+
+ start(task.get());
+
if (task->isRunning())
- {
loop.exec();
- }
+
return task->wasSuccessful();
}
+void ProgressWidget::show()
+{
+ setHidden(false);
+}
+void ProgressWidget::hide()
+{
+ setHidden(true);
+}
+
void ProgressWidget::handleTaskFinish()
{
- if (!m_task->wasSuccessful())
- {
+ if (!m_task->wasSuccessful() && m_label)
m_label->setText(m_task->failReason());
- }
+
+ if (m_hide_if_inactive)
+ hide();
}
-void ProgressWidget::handleTaskStatus(const QString &status)
+void ProgressWidget::handleTaskStatus(const QString& status)
{
- m_label->setText(status);
+ if (m_label)
+ m_label->setText(status);
}
void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)
{
diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h
index fa67748a..4d9097b8 100644
--- a/launcher/ui/widgets/ProgressWidget.h
+++ b/launcher/ui/widgets/ProgressWidget.h
@@ -9,24 +9,48 @@ class Task;
class QProgressBar;
class QLabel;
-class ProgressWidget : public QWidget
-{
+class ProgressWidget : public QWidget {
Q_OBJECT
-public:
- explicit ProgressWidget(QWidget *parent = nullptr);
+ public:
+ explicit ProgressWidget(QWidget* parent = nullptr, bool show_label = true);
-public slots:
- void start(std::shared_ptr<Task> task);
+ /** Whether to hide the widget automatically if it's watching no running task. */
+ void hideIfInactive(bool hide) { m_hide_if_inactive = hide; }
+
+ /** Reset the displayed progress to 0 */
+ void reset();
+
+ /** The text that shows up in the middle of the progress bar.
+ * By default it's '%p%', with '%p' being the total progress in percentage.
+ */
+ void progressFormat(QString);
+
+ public slots:
+ /** Watch the progress of a task. */
+ void watch(Task* task);
+
+ /** Watch the progress of a task, and start it if needed */
+ void start(Task* task);
+
+ /** Blocking way of waiting for a task to finish. */
bool exec(std::shared_ptr<Task> task);
-private slots:
+ /** Un-hide the widget if needed. */
+ void show();
+
+ /** Make the widget invisible. */
+ void hide();
+
+ private slots:
void handleTaskFinish();
- void handleTaskStatus(const QString &status);
+ void handleTaskStatus(const QString& status);
void handleTaskProgress(qint64 current, qint64 total);
void taskDestroyed();
-private:
- QLabel *m_label;
- QProgressBar *m_bar;
- std::shared_ptr<Task> m_task;
+ private:
+ QLabel* m_label = nullptr;
+ QProgressBar* m_bar = nullptr;
+ Task* m_task = nullptr;
+
+ bool m_hide_if_inactive = false;
};
diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp
new file mode 100644
index 00000000..56ae35fb
--- /dev/null
+++ b/launcher/ui/widgets/ProjectItem.cpp
@@ -0,0 +1,78 @@
+#include "ProjectItem.h"
+
+#include "Common.h"
+
+#include <QIcon>
+#include <QPainter>
+
+ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {}
+
+void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ painter->save();
+
+ QStyleOptionViewItem opt(option);
+ initStyleOption(&opt, index);
+
+ auto& rect = opt.rect;
+ auto icon_width = rect.height(), icon_height = rect.height();
+ auto remaining_width = rect.width() - icon_width;
+
+ if (opt.state & QStyle::State_Selected) {
+ painter->fillRect(rect, opt.palette.highlight());
+ painter->setPen(opt.palette.highlightedText().color());
+ } else if (opt.state & QStyle::State_MouseOver) {
+ painter->fillRect(rect, opt.palette.window());
+ }
+
+ { // Icon painting
+ // Square-sized, occupying the left portion
+ opt.icon.paint(painter, rect.x(), rect.y(), icon_width, icon_height);
+ }
+
+ { // Title painting
+ auto title = index.data(UserDataTypes::TITLE).toString();
+
+ painter->save();
+
+ auto font = opt.font;
+ if (index.data(UserDataTypes::SELECTED).toBool()) {
+ // Set nice font
+ font.setBold(true);
+ font.setUnderline(true);
+ }
+
+ font.setPointSize(font.pointSize() + 2);
+ painter->setFont(font);
+
+ // On the top, aligned to the left after the icon
+ painter->drawText(rect.x() + icon_width, rect.y() + QFontMetrics(font).height(), title);
+
+ painter->restore();
+ }
+
+ { // Description painting
+ auto description = index.data(UserDataTypes::DESCRIPTION).toString();
+
+ QTextLayout text_layout(description, opt.font);
+
+ qreal height = 0;
+ auto cut_text = viewItemTextLayout(text_layout, remaining_width, height);
+
+ // Get first line unconditionally
+ description = cut_text.first().second;
+ // Get second line, elided if needed
+ if (cut_text.size() > 1) {
+ if (cut_text.size() > 2)
+ description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first);
+ else
+ description += cut_text.at(1).second;
+ }
+
+ // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare)
+ painter->drawText(rect.x() + icon_width, rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width,
+ 2 * opt.fontMetrics.height(), Qt::TextWordWrap, description);
+ }
+
+ painter->restore();
+}
diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h
new file mode 100644
index 00000000..f668edf6
--- /dev/null
+++ b/launcher/ui/widgets/ProjectItem.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QStyledItemDelegate>
+
+/* Custom data types for our custom list models :) */
+enum UserDataTypes {
+ TITLE = 257, // QString
+ DESCRIPTION = 258, // QString
+ SELECTED = 259 // bool
+};
+
+/** This is an item delegate composed of:
+ * - An Icon on the left
+ * - A title
+ * - A description
+ * */
+class ProjectItemDelegate final : public QStyledItemDelegate {
+ Q_OBJECT
+
+ public:
+ ProjectItemDelegate(QWidget* parent);
+
+ void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override;
+
+};
diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp
index fa6e5a97..78d979ff 100644
--- a/launcher/updater/UpdateChecker.cpp
+++ b/launcher/updater/UpdateChecker.cpp
@@ -25,12 +25,11 @@
#include "BuildConfig.h"
-UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild)
+UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel)
{
m_network = nam;
m_channelUrl = channelUrl;
m_currentChannel = currentChannel;
- m_currentBuild = currentBuild;
#ifdef Q_OS_MAC
m_externalUpdater = new MacSparkleUpdater();
diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h
index 94e4312b..42ef318b 100644
--- a/launcher/updater/UpdateChecker.h
+++ b/launcher/updater/UpdateChecker.h
@@ -28,7 +28,7 @@ class UpdateChecker : public QObject
Q_OBJECT
public:
- UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild);
+ UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel);
void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate);
/*!