diff options
Diffstat (limited to 'launcher')
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 ¤t, 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 ¤t, 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 ¤t) { - 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 ¤t); 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); /*! |