diff options
Diffstat (limited to 'api')
260 files changed, 8788 insertions, 3106 deletions
diff --git a/api/gui/SkinUtils.cpp b/api/gui/SkinUtils.cpp index 79edb4b9..ec969889 100644 --- a/api/gui/SkinUtils.cpp +++ b/api/gui/SkinUtils.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -18,6 +18,7 @@ #include "Env.h" #include <QFile> +#include <QPainter> #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> @@ -35,10 +36,14 @@ QPixmap getFaceFromCache(QString username, int height, int width) if (fskin.exists()) { - QPixmap skin(fskin.fileName()); - if(!skin.isNull()) + QPixmap skinTexture(fskin.fileName()); + if(!skinTexture.isNull()) { - return skin.copy(8, 8, 8, 8).scaled(height, width, Qt::KeepAspectRatio); + QPixmap skin = QPixmap(8, 8); + QPainter painter(&skin); + painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); + painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); + return skin.scaled(height, width, Qt::KeepAspectRatio); } } diff --git a/api/gui/SkinUtils.h b/api/gui/SkinUtils.h index f042b908..b44f4228 100644 --- a/api/gui/SkinUtils.h +++ b/api/gui/SkinUtils.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/gui/icons/IconList.cpp b/api/gui/icons/IconList.cpp index 5bf5f5e7..70350534 100644 --- a/api/gui/icons/IconList.cpp +++ b/api/gui/icons/IconList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -187,8 +187,7 @@ Qt::DropActions IconList::supportedDropActions() const return Qt::CopyAction; } -bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, - const QModelIndex &parent) +bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (action == Qt::IgnoreAction) return true; diff --git a/api/gui/icons/IconList.h b/api/gui/icons/IconList.h index 97ed3956..f07415fa 100644 --- a/api/gui/icons/IconList.h +++ b/api/gui/icons/IconList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -80,7 +80,7 @@ protected slots: void fileChanged(const QString &path); void SettingChanged(const Setting & setting, QVariant value); private: - std::shared_ptr<QFileSystemWatcher> m_watcher; + shared_qobject_ptr<QFileSystemWatcher> m_watcher; bool is_watching; QMap<QString, int> name_index; QVector<MMCIcon> icons; diff --git a/api/gui/icons/MMCIcon.cpp b/api/gui/icons/MMCIcon.cpp index a5ab548e..f0b82ec9 100644 --- a/api/gui/icons/MMCIcon.cpp +++ b/api/gui/icons/MMCIcon.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -102,3 +102,17 @@ void MMCIcon::replace(IconType new_type, const QString& key) m_images[new_type].filename = QString(); m_images[new_type].key = key; } + +QString MMCIcon::getFilePath() const +{ + if(m_current_type == IconType::ToBeDeleted){ + return QString(); + } + return m_images[m_current_type].filename; +} + + +bool MMCIcon::isBuiltIn() const +{ + return m_current_type == IconType::Builtin; +} diff --git a/api/gui/icons/MMCIcon.h b/api/gui/icons/MMCIcon.h index d7618bee..fd14b5b7 100644 --- a/api/gui/icons/MMCIcon.h +++ b/api/gui/icons/MMCIcon.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -46,4 +46,6 @@ struct MULTIMC_GUI_EXPORT MMCIcon void remove(IconType rm_type); void replace(IconType new_type, QIcon icon, QString path = QString()); void replace(IconType new_type, const QString &key); + bool isBuiltIn() const; + QString getFilePath() const; }; diff --git a/api/logic/BaseInstaller.cpp b/api/logic/BaseInstaller.cpp index e25683a9..d61c3fe9 100644 --- a/api/logic/BaseInstaller.cpp +++ b/api/logic/BaseInstaller.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/BaseInstaller.h b/api/logic/BaseInstaller.h index 121d35ef..3e40b355 100644 --- a/api/logic/BaseInstaller.h +++ b/api/logic/BaseInstaller.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/BaseInstance.cpp b/api/logic/BaseInstance.cpp index 7ddcc549..46b45827 100644 --- a/api/logic/BaseInstance.cpp +++ b/api/logic/BaseInstance.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -37,6 +37,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("notes", ""); m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); + m_settings->registerSetting("lastTimePlayed", 0); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); @@ -134,15 +135,24 @@ void BaseInstance::setRunning(bool running) m_isRunning = running; + if(!m_settings->get("RecordGameTime").toBool()) + { + emit runningStatusChanged(running); + return; + } + if(running) { m_timeStarted = QDateTime::currentDateTime(); } else { - qint64 current = settings()->get("totalTimePlayed").toLongLong(); QDateTime timeEnded = QDateTime::currentDateTime(); + + qint64 current = settings()->get("totalTimePlayed").toLongLong(); settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); + settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); + emit propertiesChanged(this); } @@ -160,9 +170,20 @@ int64_t BaseInstance::totalTimePlayed() const return current; } +int64_t BaseInstance::lastTimePlayed() const +{ + if(m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return m_timeStarted.secsTo(timeNow); + } + return settings()->get("lastTimePlayed").toLongLong(); +} + void BaseInstance::resetTimePlayed() { settings()->reset("totalTimePlayed"); + settings()->reset("lastTimePlayed"); } QString BaseInstance::instanceType() const @@ -175,11 +196,6 @@ QString BaseInstance::instanceRoot() const return m_rootDir; } -InstancePtr BaseInstance::getSharedPtr() -{ - return shared_from_this(); -} - SettingsObjectPtr BaseInstance::settings() const { return m_settings; @@ -253,7 +269,7 @@ QStringList BaseInstance::extraArguments() const return Commandline::splitArgs(settings()->get("JvmArgs").toString()); } -std::shared_ptr<LaunchTask> BaseInstance::getLaunchTask() +shared_qobject_ptr<LaunchTask> BaseInstance::getLaunchTask() { return m_launchProcess; } diff --git a/api/logic/BaseInstance.h b/api/logic/BaseInstance.h index 04788eb7..d250e03e 100644 --- a/api/logic/BaseInstance.h +++ b/api/logic/BaseInstance.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -34,6 +34,8 @@ #include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" + class QDir; class Task; class LaunchTask; @@ -85,6 +87,7 @@ public: void setRunning(bool running); bool isRunning() const; int64_t totalTimePlayed() const; + int64_t lastTimePlayed() const; void resetTimePlayed(); /// get the type of this instance @@ -134,8 +137,6 @@ public: /// Sets the last launched time to 'val' milliseconds since epoch void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch()); - InstancePtr getSharedPtr(); - /*! * \brief Gets this instance's settings object. * This settings object stores instance-specific settings. @@ -147,10 +148,11 @@ public: virtual shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) = 0; /// returns a valid launcher (task container) - virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) = 0; + virtual shared_qobject_ptr<LaunchTask> createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0; /// returns the current launch task (if any) - std::shared_ptr<LaunchTask> getLaunchTask(); + shared_qobject_ptr<LaunchTask> getLaunchTask(); /*! * Create envrironment variables for running the instance @@ -223,9 +225,9 @@ public: bool reloadSettings(); /** - * 'print' a verbose desription of the instance into a QStringList + * 'print' a verbose description of the instance into a QStringList */ - virtual QStringList verboseDescription(AuthSessionPtr session) = 0; + virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) = 0; Status currentStatus() const; @@ -241,7 +243,7 @@ signals: */ void propertiesChanged(BaseInstance *inst); - void launchTaskChanged(std::shared_ptr<LaunchTask>); + void launchTaskChanged(shared_qobject_ptr<LaunchTask>); void runningStatusChanged(bool running); @@ -255,7 +257,7 @@ protected: /* data */ SettingsObjectPtr m_settings; // InstanceFlags m_flags; bool m_isRunning = false; - std::shared_ptr<LaunchTask> m_launchProcess; + shared_qobject_ptr<LaunchTask> m_launchProcess; QDateTime m_timeStarted; private: /* data */ @@ -265,6 +267,6 @@ private: /* data */ bool m_hasBrokenVersion = false; }; -Q_DECLARE_METATYPE(std::shared_ptr<BaseInstance>) +Q_DECLARE_METATYPE(shared_qobject_ptr<BaseInstance>) //Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) //Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) diff --git a/api/logic/BaseVersion.h b/api/logic/BaseVersion.h index 4f06c3bc..b88105fb 100644 --- a/api/logic/BaseVersion.h +++ b/api/logic/BaseVersion.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/BaseVersionList.cpp b/api/logic/BaseVersionList.cpp index b1f73529..aa9cb6cf 100644 --- a/api/logic/BaseVersionList.cpp +++ b/api/logic/BaseVersionList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/BaseVersionList.h b/api/logic/BaseVersionList.h index a70a414c..29e21bdb 100644 --- a/api/logic/BaseVersionList.h +++ b/api/logic/BaseVersionList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 5d20b7a9..6d269714 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -119,13 +119,13 @@ set(NET_SOURCES net/PasteUpload.cpp net/PasteUpload.h net/Sink.h - net/URLConstants.cpp - net/URLConstants.h net/Validator.h ) # Game launch logic set(LAUNCH_SOURCES + launch/steps/LookupServerAddress.cpp + launch/steps/LookupServerAddress.h launch/steps/PostLaunchCommand.cpp launch/steps/PostLaunchCommand.h launch/steps/PreLaunchCommand.cpp @@ -182,9 +182,11 @@ set(NEWS_SOURCES # Icon interface set(ICONS_SOURCES - # News System + # Icons System and related code icons/IIconList.h icons/IIconList.cpp + icons/IconUtils.h + icons/IconUtils.cpp ) # Minecraft services status checker @@ -211,6 +213,10 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/RefreshTask.cpp minecraft/auth/flows/ValidateTask.h minecraft/auth/flows/ValidateTask.cpp + + minecraft/gameoptions/GameOptions.h + minecraft/gameoptions/GameOptions.cpp + minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp minecraft/update/FMLLibrariesTask.cpp @@ -219,10 +225,11 @@ set(MINECRAFT_SOURCES minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp minecraft/update/LibrariesTask.h + minecraft/launch/ClaimAccount.cpp minecraft/launch/ClaimAccount.h - minecraft/launch/CreateServerResourcePacksFolder.cpp - minecraft/launch/CreateServerResourcePacksFolder.h + minecraft/launch/CreateGameFolders.cpp + minecraft/launch/CreateGameFolders.h minecraft/launch/ModMinecraftJar.cpp minecraft/launch/ModMinecraftJar.h minecraft/launch/DirectJavaLaunch.cpp @@ -231,14 +238,24 @@ set(MINECRAFT_SOURCES minecraft/launch/ExtractNatives.h minecraft/launch/LauncherPartLaunch.cpp minecraft/launch/LauncherPartLaunch.h + minecraft/launch/MinecraftServerTarget.cpp + minecraft/launch/MinecraftServerTarget.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h + minecraft/launch/ReconstructAssets.cpp + minecraft/launch/ReconstructAssets.h + minecraft/launch/ScanModFolders.cpp + minecraft/launch/ScanModFolders.h + minecraft/launch/VerifyJavaInstall.cpp + minecraft/launch/VerifyJavaInstall.h + minecraft/legacy/LegacyModList.h minecraft/legacy/LegacyModList.cpp minecraft/legacy/LegacyInstance.h minecraft/legacy/LegacyInstance.cpp minecraft/legacy/LegacyUpgradeTask.h minecraft/legacy/LegacyUpgradeTask.cpp + minecraft/GradleSpecifier.h minecraft/MinecraftInstance.cpp minecraft/MinecraftInstance.h @@ -246,8 +263,8 @@ set(MINECRAFT_SOURCES minecraft/LaunchProfile.h minecraft/Component.cpp minecraft/Component.h - minecraft/ComponentList.cpp - minecraft/ComponentList.h + minecraft/PackProfile.cpp + minecraft/PackProfile.h minecraft/ComponentUpdateTask.cpp minecraft/ComponentUpdateTask.h minecraft/MinecraftLoadAndCheck.h @@ -273,28 +290,37 @@ set(MINECRAFT_SOURCES minecraft/VersionFile.h minecraft/VersionFilterData.h minecraft/VersionFilterData.cpp - minecraft/Mod.h - minecraft/Mod.cpp - minecraft/ModsModel.h - minecraft/ModsModel.cpp - minecraft/SimpleModList.h - minecraft/SimpleModList.cpp minecraft/World.h minecraft/World.cpp minecraft/WorldList.h minecraft/WorldList.cpp + minecraft/mod/Mod.h + minecraft/mod/Mod.cpp + minecraft/mod/ModDetails.h + minecraft/mod/ModFolderModel.h + minecraft/mod/ModFolderModel.cpp + minecraft/mod/ModFolderLoadTask.h + minecraft/mod/ModFolderLoadTask.cpp + minecraft/mod/LocalModParseTask.h + minecraft/mod/LocalModParseTask.cpp + minecraft/mod/ResourcePackFolderModel.h + minecraft/mod/ResourcePackFolderModel.cpp + minecraft/mod/TexturePackFolderModel.h + minecraft/mod/TexturePackFolderModel.cpp + # Assets minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp - # Forge and all things forge related - minecraft/forge/ForgeXzDownload.h - minecraft/forge/ForgeXzDownload.cpp + # Minecraft services + minecraft/services/SkinUpload.cpp + minecraft/services/SkinUpload.h + minecraft/services/SkinDelete.cpp + minecraft/services/SkinDelete.h - # Skin upload utilities - minecraft/SkinUpload.cpp - minecraft/SkinUpload.h + mojang/PackageManifest.h + mojang/PackageManifest.cpp ) add_unit_test(GradleSpecifier @@ -302,6 +328,22 @@ add_unit_test(GradleSpecifier LIBS MultiMC_logic ) +add_executable(PackageManifest + mojang/PackageManifest_test.cpp +) +target_link_libraries(PackageManifest + MultiMC_logic + Qt5::Test +) +target_include_directories(PackageManifest + PRIVATE ../../cmake/UnitTest/ +) +add_test( + NAME PackageManifest + COMMAND PackageManifest + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + add_unit_test(MojangVersionFormat SOURCES minecraft/MojangVersionFormat_test.cpp LIBS MultiMC_logic @@ -314,8 +356,8 @@ add_unit_test(Library ) # FIXME: shares data with FileSystem test -add_unit_test(SimpleModList - SOURCES minecraft/SimpleModList_test.cpp +add_unit_test(ModFolderModel + SOURCES minecraft/mod/ModFolderModel_test.cpp DATA testdata LIBS MultiMC_logic ) @@ -389,6 +431,8 @@ add_unit_test(JavaVersion set(TRANSLATIONS_SOURCES translations/TranslationsModel.h translations/TranslationsModel.cpp + translations/POTranslator.h + translations/POTranslator.cpp ) set(TOOLS_SOURCES @@ -420,25 +464,51 @@ set(META_SOURCES ) set(FTB_SOURCES - modplatform/ftb/FtbPackFetchTask.h - modplatform/ftb/FtbPackFetchTask.cpp - modplatform/ftb/FtbPackInstallTask.h - modplatform/ftb/FtbPackInstallTask.cpp - - modplatform/ftb/FtbPrivatePackManager.h - modplatform/ftb/FtbPrivatePackManager.cpp - - modplatform/ftb/PackHelpers.h + modplatform/legacy_ftb/PackFetchTask.h + modplatform/legacy_ftb/PackFetchTask.cpp + modplatform/legacy_ftb/PackInstallTask.h + modplatform/legacy_ftb/PackInstallTask.cpp + modplatform/legacy_ftb/PrivatePackManager.h + modplatform/legacy_ftb/PrivatePackManager.cpp + + modplatform/legacy_ftb/PackHelpers.h ) set(FLAME_SOURCES # Flame + modplatform/flame/FlamePackIndex.cpp + modplatform/flame/FlamePackIndex.h modplatform/flame/PackManifest.h modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp ) +set(MODPACKSCH_SOURCES + modplatform/modpacksch/FTBPackInstallTask.h + modplatform/modpacksch/FTBPackInstallTask.cpp + modplatform/modpacksch/FTBPackManifest.h + modplatform/modpacksch/FTBPackManifest.cpp +) + +set(TECHNIC_SOURCES + modplatform/technic/SingleZipPackInstallTask.h + modplatform/technic/SingleZipPackInstallTask.cpp + modplatform/technic/SolderPackInstallTask.h + modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/TechnicPackProcessor.h + modplatform/technic/TechnicPackProcessor.cpp +) + +set(ATLAUNCHER_SOURCES + modplatform/atlauncher/ATLPackIndex.cpp + modplatform/atlauncher/ATLPackIndex.h + modplatform/atlauncher/ATLPackInstallTask.cpp + modplatform/atlauncher/ATLPackInstallTask.h + modplatform/atlauncher/ATLPackManifest.cpp + modplatform/atlauncher/ATLPackManifest.h +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS MultiMC_logic @@ -469,6 +539,9 @@ set(LOGIC_SOURCES ${ICONS_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} + ${MODPACKSCH_SOURCES} + ${TECHNIC_SOURCES} + ${ATLAUNCHER_SOURCES} ) add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) @@ -477,7 +550,7 @@ set_target_properties(MultiMC_logic PROPERTIES CXX_VISIBILITY_PRESET hidden VISI generate_export_header(MultiMC_logic) # Link -target_link_libraries(MultiMC_logic xz-embedded MultiMC_unpack200 systeminfo MultiMC_quazip MultiMC_classparser ${NBT_NAME} ${ZLIB_LIBRARIES}) +target_link_libraries(MultiMC_logic systeminfo MultiMC_quazip MultiMC_classparser ${NBT_NAME} ${ZLIB_LIBRARIES} optional-bare tomlc99 BuildConfig) target_link_libraries(MultiMC_logic Qt5::Core Qt5::Xml Qt5::Network Qt5::Concurrent) # Mark and export headers diff --git a/api/logic/Commandline.cpp b/api/logic/Commandline.cpp index 81ae7d0b..2c0fde64 100644 --- a/api/logic/Commandline.cpp +++ b/api/logic/Commandline.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * diff --git a/api/logic/Commandline.h b/api/logic/Commandline.h index 586d644f..09c1707e 100644 --- a/api/logic/Commandline.h +++ b/api/logic/Commandline.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * @@ -69,8 +69,8 @@ namespace ArgumentStyle { enum Enum { - Space, /**< --option=value */ - Equals, /**< --option value */ + Space, /**< --option value */ + Equals, /**< --option=value */ SpaceAndEquals, /**< --option[= ]value */ #ifdef Q_OS_WIN32 Default = Equals diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp index 77546bbc..71b49d95 100644 --- a/api/logic/Env.cpp +++ b/api/logic/Env.cpp @@ -96,7 +96,11 @@ void Env::initHttpMetaCache() m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath()); m_metacache->addBase("general", QDir("cache").absolutePath()); + m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath()); m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); + 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("skins", QDir("accounts/skins").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); @@ -159,11 +163,9 @@ void Env::updateProxySettings(QString proxyTypeStr, QString addr, int port, QStr proxyDesc = "DERP proxy: "; break; } - proxyDesc += QString("%3@%1:%2 pass %4") + proxyDesc += QString("%1:%2") .arg(proxy.hostName()) - .arg(proxy.port()) - .arg(proxy.user()) - .arg(proxy.password()); + .arg(proxy.port()); qDebug() << proxyDesc; } diff --git a/api/logic/FileSystem.cpp b/api/logic/FileSystem.cpp index 192d868b..13f05b86 100644 --- a/api/logic/FileSystem.cpp +++ b/api/logic/FileSystem.cpp @@ -174,6 +174,11 @@ bool copy::operator()(const QString &offset) bool deletePath(QString path) { bool OK = true; + QFileInfo finfo(path); + if(finfo.isFile()) { + return QFile::remove(path); + } + QDir dir(path); if (!dir.exists()) @@ -294,7 +299,7 @@ QString NormalizePath(QString path) } } -QString badFilenameChars = "\"\\/?<>:*|!+\r\n"; +QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { diff --git a/api/logic/InstanceCopyTask.cpp b/api/logic/InstanceCopyTask.cpp index a576a0fd..35adeaf9 100644 --- a/api/logic/InstanceCopyTask.cpp +++ b/api/logic/InstanceCopyTask.cpp @@ -5,9 +5,10 @@ #include "pathmatcher/RegexpMatcher.h" #include <QtConcurrentRun> -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves) +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime) { m_origInstance = origInstance; + m_keepPlaytime = keepPlaytime; if(!copySaves) { @@ -46,6 +47,9 @@ void InstanceCopyTask::copyFinished() InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); inst->setName(m_instName); inst->setIconKey(m_instIcon); + if(!m_keepPlaytime) { + inst->resetTimePlayed(); + } emitSucceeded(); } diff --git a/api/logic/InstanceCopyTask.h b/api/logic/InstanceCopyTask.h index 8dd55b40..6465e92d 100644 --- a/api/logic/InstanceCopyTask.h +++ b/api/logic/InstanceCopyTask.h @@ -15,7 +15,7 @@ class MULTIMC_LOGIC_EXPORT InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves); + explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime); protected: //! Entry point for tasks. @@ -28,4 +28,5 @@ private: /* data */ QFuture<bool> m_copyFuture; QFutureWatcher<bool> m_copyFutureWatcher; std::unique_ptr<IPathMatcher> m_matcher; + bool m_keepPlaytime; }; diff --git a/api/logic/InstanceCreationTask.cpp b/api/logic/InstanceCreationTask.cpp index 978d4687..eafc5126 100644 --- a/api/logic/InstanceCreationTask.cpp +++ b/api/logic/InstanceCreationTask.cpp @@ -4,7 +4,7 @@ //FIXME: remove this #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version) { @@ -20,7 +20,7 @@ void InstanceCreationTask::executeTask() instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); - auto components = inst.getComponentList(); + auto components = inst.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_version->descriptor(), true); inst.setName(m_instName); diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index bbab557a..3eac4d57 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -1,3 +1,18 @@ +/* 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 "InstanceImportTask.h" #include "BaseInstance.h" #include "FileSystem.h" @@ -6,14 +21,17 @@ #include "NullInstance.h" #include "settings/INISettingsObject.h" #include "icons/IIconList.h" +#include "icons/IconUtils.h" #include <QtConcurrentRun> // FIXME: this does not belong here, it's Minecraft/Flame specific #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" #include "Json.h" +#include <quazipdir.h> +#include "modplatform/technic/TechnicPackProcessor.h" InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { @@ -22,8 +40,6 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) void InstanceImportTask::executeTask() { - InstancePtr newInstance; - if (m_sourceUrl.isLocalFile()) { m_archivePath = m_sourceUrl.toLocalFile(); @@ -81,6 +97,7 @@ void InstanceImportTask::processZipPack() QStringList blacklist = {"instance.cfg", "manifest.json"}; QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); + bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); QString root; if(!mmcFound.isNull()) @@ -90,6 +107,14 @@ void InstanceImportTask::processZipPack() root = mmcFound; m_modpackType = ModpackType::MultiMC; } + else if (technicFound) + { + // process as Technic pack + qDebug() << "Technic:" << technicFound; + extractDir.mkpath(".minecraft"); + extractDir.cd(".minecraft"); + m_modpackType = ModpackType::Technic; + } else if(!flameFound.isNull()) { // process as Flame pack @@ -97,7 +122,6 @@ void InstanceImportTask::processZipPack() root = flameFound; m_modpackType = ModpackType::Flame; } - if(m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -114,7 +138,7 @@ void InstanceImportTask::processZipPack() void InstanceImportTask::extractFinished() { m_packZip.reset(); - if (m_extractFuture.result().isEmpty()) + if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; @@ -160,6 +184,9 @@ void InstanceImportTask::extractFinished() case ModpackType::MultiMC: processMultiMC(); return; + case ModpackType::Technic: + processTechnic(); + return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; @@ -211,6 +238,7 @@ void InstanceImportTask::processFlame() } QString forgeVersion; + QString fabricVersion; for(auto &loader: pack.minecraft.modLoaders) { auto id = loader.id; @@ -220,6 +248,12 @@ void InstanceImportTask::processFlame() forgeVersion = id; continue; } + if(id.startsWith("fabric-")) + { + id.remove("fabric-"); + fabricVersion = id; + continue; + } logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); } @@ -235,7 +269,7 @@ void InstanceImportTask::processFlame() mcVersion.remove(QRegExp("[.]+$")); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } - auto components = instance.getComponentList(); + auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", mcVersion, true); if(!forgeVersion.isEmpty()) @@ -254,6 +288,10 @@ void InstanceImportTask::processFlame() } components->setComponentVersion("net.minecraftforge", forgeVersion); } + if(!fabricVersion.isEmpty()) + { + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); + } if (m_instIcon != "default") { instance.setIconKey(m_instIcon); @@ -287,7 +325,7 @@ void InstanceImportTask::processFlame() qDebug() << info.fileName(); jarMods.push_back(info.absoluteFilePath()); } - auto profile = instance.getComponentList(); + auto profile = instance.getPackProfile(); profile->installJarMods(jarMods); // nuke the original files FS::deletePath(jarmodsPath); @@ -370,6 +408,14 @@ void InstanceImportTask::processFlame() m_modIdResolver->start(); } +void InstanceImportTask::processTechnic() +{ + shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath); +} + void InstanceImportTask::processMultiMC() { // FIXME: copy from FolderInstanceProvider!!! FIX IT!!! @@ -393,8 +439,9 @@ void InstanceImportTask::processMultiMC() else { m_instIcon = instance.iconKey(); - auto importIconPath = FS::PathCombine(instance.instanceRoot(), m_instIcon + ".png"); - if (QFile::exists(importIconPath)) + + auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = ENV.icons(); diff --git a/api/logic/InstanceImportTask.h b/api/logic/InstanceImportTask.h index d326391b..7291324d 100644 --- a/api/logic/InstanceImportTask.h +++ b/api/logic/InstanceImportTask.h @@ -1,3 +1,18 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "InstanceTask.h" @@ -9,6 +24,8 @@ #include "settings/SettingsObject.h" #include "QObjectPtr.h" +#include <nonstd/optional> + class QuaZip; namespace Flame { @@ -29,6 +46,7 @@ private: void processZipPack(); void processMultiMC(); void processFlame(); + void processTechnic(); private slots: void downloadSucceeded(); @@ -44,11 +62,12 @@ private: /* data */ QString m_archivePath; bool m_downloadRequired = false; std::unique_ptr<QuaZip> m_packZip; - QFuture<QStringList> m_extractFuture; - QFutureWatcher<QStringList> m_extractFutureWatcher; + QFuture<nonstd::optional<QStringList>> m_extractFuture; + QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; enum class ModpackType{ Unknown, MultiMC, - Flame + Flame, + Technic } m_modpackType = ModpackType::Unknown; }; diff --git a/api/logic/InstanceList.cpp b/api/logic/InstanceList.cpp index 4a209454..02fae6ac 100644 --- a/api/logic/InstanceList.cpp +++ b/api/logic/InstanceList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -95,10 +95,15 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const { return pdata->id(); } + case Qt::EditRole: case Qt::DisplayRole: { return pdata->name(); } + case Qt::AccessibleTextRole: + { + return tr("%1 Instance").arg(pdata->name()); + } case Qt::ToolTipRole: { return pdata->instanceRoot(); @@ -118,12 +123,32 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const return QVariant(); } +bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid()) + { + return false; + } + if(role != Qt::EditRole) + { + return false; + } + BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer()); + auto newName = value.toString(); + if(pdata->name() == newName) + { + return true; + } + pdata->setName(newName); + return true; +} + Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const { Qt::ItemFlags f; if (index.isValid()) { - f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable); + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; } @@ -135,8 +160,8 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const { return GroupId(); } - auto iter = m_groupMap.find(inst->id()); - if(iter != m_groupMap.end()) + auto iter = m_instanceGroupIndex.find(inst->id()); + if(iter != m_instanceGroupIndex.end()) { return *iter; } @@ -153,8 +178,8 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) } bool changed = false; - auto iter = m_groupMap.find(inst->id()); - if(iter != m_groupMap.end()) + auto iter = m_instanceGroupIndex.find(inst->id()); + if(iter != m_instanceGroupIndex.end()) { if(*iter != name) { @@ -165,19 +190,21 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) else { changed = true; - m_groupMap[id] = name; + m_instanceGroupIndex[id] = name; } if(changed) { - m_groups.insert(name); + m_groupNameCache.insert(name); + auto idx = getInstIndex(inst.get()); + emit dataChanged(index(idx), index(idx), {GroupRole}); saveGroupList(); } } QStringList InstanceList::getGroups() { - return m_groups.toList(); + return m_groupNameCache.toList(); } void InstanceList::deleteGroup(const QString& name) @@ -190,7 +217,7 @@ void InstanceList::deleteGroup(const QString& name) auto instGroupName = getInstanceGroup(instID); if(instGroupName == name) { - m_groupMap.remove(instID); + m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); @@ -206,16 +233,21 @@ void InstanceList::deleteGroup(const QString& name) } } +bool InstanceList::isGroupCollapsed(const QString& group) +{ + return m_collapsedGroups.contains(group); +} + void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); if(!inst) { - qDebug() << "Cannot delete instance" << id << " No such instance is present."; + qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } - if(m_groupMap.remove(id)) + if(m_instanceGroupIndex.remove(id)) { saveGroupList(); } @@ -488,7 +520,7 @@ void InstanceList::saveGroupList() WatchLock foo(m_watcher, m_instDir); QString groupFileName = m_instDir + "/instgroups.json"; QMap<QString, QSet<QString>> reverseGroupMap; - for (auto iter = m_groupMap.begin(); iter != m_groupMap.end(); iter++) + for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { QString id = iter.key(); QString group = iter.value(); @@ -521,7 +553,7 @@ void InstanceList::saveGroupList() auto name = iter.key(); QJsonObject groupObj; QJsonArray instanceArr; - groupObj.insert("hidden", QJsonValue(QString("false"))); + groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); for (auto item : list) { instanceArr.append(QJsonValue(item)); @@ -545,7 +577,6 @@ void InstanceList::saveGroupList() void InstanceList::loadGroupList() { qDebug() << "Will load group list now."; - QSet<QString> groupSet; QString groupFileName = m_instDir + "/instgroups.json"; @@ -596,7 +627,8 @@ void InstanceList::loadGroupList() return; } - m_groupMap.clear(); + QSet<QString> groupSet; + m_instanceGroupIndex.clear(); // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); @@ -607,37 +639,35 @@ void InstanceList::loadGroupList() // If not an object, complain and skip to the next one. if (!iter.value().isObject()) { - qWarning() << QString("Group '%1' in the group list should " - "be an object.") - .arg(groupName) - .toUtf8(); + 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(); + qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8(); continue; } // keep a list/set of groups for choosing groupSet.insert(groupName); + auto hidden = groupObj.value("hidden").toBool(false); + 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++) { - m_groupMap[(*iter2).toString()] = groupName; + m_instanceGroupIndex[(*iter2).toString()] = groupName; } } m_groupsLoaded = true; - m_groups.unite(groupSet); + m_groupNameCache.unite(groupSet); qDebug() << "Group list loaded."; } @@ -662,6 +692,17 @@ 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) { + m_collapsedGroups.insert(group); + } else { + m_collapsedGroups.remove(group); + } + saveGroupList(); +} + class InstanceStaging : public Task { Q_OBJECT @@ -691,6 +732,25 @@ public: virtual ~InstanceStaging() {}; + + // FIXME/TODO: add ability to abort during instance commit retries + bool abort() override + { + if(m_child && m_child->canAbort()) + { + return m_child->abort(); + } + return false; + } + bool canAbort() const override + { + if(m_child && m_child->canAbort()) + { + return true; + } + return false; + } + protected: virtual void executeTask() override { @@ -773,10 +833,11 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst qWarning() << "Failed to move" << path << "to" << destination; return false; } - m_groupMap[instID] = groupName; + m_instanceGroupIndex[instID] = groupName; instanceSet.insert(instID); - m_groups.insert(groupName); + m_groupNameCache.insert(groupName); emit instancesChanged(); + emit instanceSelectRequest(instID); } saveGroupList(); return true; diff --git a/api/logic/InstanceList.h b/api/logic/InstanceList.h index 5b966b33..8215cb66 100644 --- a/api/logic/InstanceList.h +++ b/api/logic/InstanceList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -58,10 +58,12 @@ public: virtual ~InstanceList(); public: - QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const; - int rowCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - Qt::ItemFlags flags(const QModelIndex &index) const; + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + bool setData(const QModelIndex & index, const QVariant & value, int role) override; enum AdditionalRoles { @@ -97,6 +99,8 @@ public: InstancePtr getInstanceById(QString id) const; QModelIndex getInstanceIndexById(const QString &id) const; QStringList getGroups(); + bool isGroupCollapsed(const QString &groupName); + GroupId getInstanceGroup(const InstanceId & id) const; void setInstanceGroup(const InstanceId & id, const GroupId& name); @@ -127,10 +131,12 @@ public: signals: void dataIsInvalid(); void instancesChanged(); + void instanceSelectRequest(QString instanceId); void groupsChanged(QSet<QString> groups); public slots: void on_InstFolderChanged(const Setting &setting, QVariant value); + void on_GroupStateChanged(const QString &group, bool collapsed); private slots: void propertiesChanged(BaseInstance *inst); @@ -151,12 +157,14 @@ private: int m_watchLevel = 0; bool m_dirty = false; QList<InstancePtr> m_instances; - QSet<QString> m_groups; + QSet<QString> m_groupNameCache; SettingsObjectPtr m_globalSettings; QString m_instDir; QFileSystemWatcher * m_watcher; - QMap<InstanceId, GroupId> m_groupMap; + // FIXME: this is so inefficient that looking at it is almost painful. + QSet<QString> m_collapsedGroups; + QMap<InstanceId, GroupId> m_instanceGroupIndex; QSet<InstanceId> instanceSet; bool m_groupsLoaded = false; bool m_instancesProbed = false; diff --git a/api/logic/LoggedProcess.h b/api/logic/LoggedProcess.h index 2cbc28bc..327cdc6a 100644 --- a/api/logic/LoggedProcess.h +++ b/api/logic/LoggedProcess.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp index 21a55493..b25c61e7 100644 --- a/api/logic/MMCZip.cpp +++ b/api/logic/MMCZip.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -208,16 +208,27 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re // ours -QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) { QDir directory(target); QStringList extracted; + qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; - if (!zip->goToFirstFile()) + auto numEntries = zip->getEntriesCount(); + if(numEntries < 0) { + qWarning() << "Failed to enumerate files in archive"; + return nonstd::nullopt; + } + else if(numEntries == 0) { + qDebug() << "Extracting empty archives seems odd..."; + return extracted; + } + else if (!zip->goToFirstFile()) { qWarning() << "Failed to seek to first file in zip"; - return QStringList(); + return nonstd::nullopt; } + do { QString name = zip->getCurrentFileName(); @@ -235,7 +246,7 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt { qWarning() << "Failed to extract file" << name << "to" << absFilePath; JlCompress::removeFile(extracted); - return QStringList(); + return nonstd::nullopt; } extracted.append(absFilePath); qDebug() << "Extracted file" << name; @@ -244,12 +255,58 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt } // ours -QStringList MMCZip::extractDir(QString fileCompressed, QString dir) +bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) +{ + return JlCompress::extractFile(zip, file, target); +} + +// ours +nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) { - return {}; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; } return MMCZip::extractSubDir(&zip, "", dir); } + +// ours +nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; + } + return MMCZip::extractSubDir(&zip, subdir, dir); +} + +// ours +bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return true; + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + return false; + } + return MMCZip::extractRelFile(&zip, file, target); +} diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index 4eb86361..98d9cd5b 100644 --- a/api/logic/MMCZip.h +++ b/api/logic/MMCZip.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -18,12 +18,13 @@ #include <QString> #include <QFileInfo> #include <QSet> -#include "minecraft/Mod.h" +#include "minecraft/mod/Mod.h" #include <functional> #include "multimc_logic_export.h" #include <JlCompress.h> +#include <nonstd/optional> namespace MMCZip { @@ -57,7 +58,9 @@ namespace MMCZip /** * Extract a subdirectory from an archive */ - QStringList MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + nonstd::optional<QStringList> MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + + bool MULTIMC_LOGIC_EXPORT extractRelFile(QuaZip *zip, const QString & file, const QString &target); /** * Extract a whole archive. @@ -66,6 +69,26 @@ 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. */ - QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); + nonstd::optional<QStringList> MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); + + /** + * Extract a subdirectory from an archive + * + * \param fileCompressed The name of the archive. + * \param subdir The directory within the archive to extract + * \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> MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString subdir, QString dir); + + /** + * Extract a single file from an archive into a directory + * + * \param fileCompressed The name of the archive. + * \param file The file within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return true for success or false for failure + */ + bool MULTIMC_LOGIC_EXPORT extractFile(QString fileCompressed, QString file, QString dir); } diff --git a/api/logic/NullInstance.h b/api/logic/NullInstance.h index bef6bc4f..94ed6c3a 100644 --- a/api/logic/NullInstance.h +++ b/api/logic/NullInstance.h @@ -1,8 +1,10 @@ #pragma once #include "BaseInstance.h" +#include "launch/LaunchTask.h" class NullInstance: public BaseInstance { + Q_OBJECT public: NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) :BaseInstance(globalSettings, settings, rootDir) @@ -10,46 +12,46 @@ public: setVersionBroken(true); } virtual ~NullInstance() {}; - virtual void saveNow() override + void saveNow() override { } - virtual QString getStatusbarDescription() override + QString getStatusbarDescription() override { return tr("Unknown instance type"); }; - virtual QSet< QString > traits() const override + QSet< QString > traits() const override { return {}; }; - virtual QString instanceConfigFolder() const override + QString instanceConfigFolder() const override { return instanceRoot(); }; - virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr) override + shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; } - virtual shared_qobject_ptr< Task > createUpdateTask(Net::Mode mode) override + shared_qobject_ptr< Task > createUpdateTask(Net::Mode mode) override { return nullptr; } - virtual QProcessEnvironment createEnvironment() override + QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } - virtual QMap<QString, QString> getVariables() const override + QMap<QString, QString> getVariables() const override { return QMap<QString, QString>(); } - virtual IPathMatcher::Ptr getLogFileMatcher() override + IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; } - virtual QString getLogFileRoot() override + QString getLogFileRoot() override { return instanceRoot(); } - virtual QString typeName() const override + QString typeName() const override { return "Null"; } @@ -65,7 +67,7 @@ public: { return false; } - QStringList verboseDescription(AuthSessionPtr session) override + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override { QStringList out; out << "Null instance - placeholder."; diff --git a/api/logic/RWStorage.h b/api/logic/RWStorage.h index 5d792367..3028388e 100644 --- a/api/logic/RWStorage.h +++ b/api/logic/RWStorage.h @@ -1,6 +1,9 @@ #pragma once #include <QWriteLocker> #include <QReadLocker> +#include <QMap> +#include <QSet> + template <typename K, typename V> class RWStorage { diff --git a/api/logic/Version.cpp b/api/logic/Version.cpp index 42eac669..6392a50f 100644 --- a/api/logic/Version.cpp +++ b/api/logic/Version.cpp @@ -78,7 +78,7 @@ void Version::parse() // FIXME: this is bad. versions can contain a lot more separators... QStringList parts = m_string.split('.'); - for (const auto part : parts) + for (const auto &part : parts) { m_sections.append(Section(part)); } diff --git a/api/logic/Version_test.cpp b/api/logic/Version_test.cpp index bfbec12d..b2d657a6 100644 --- a/api/logic/Version_test.cpp +++ b/api/logic/Version_test.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/icons/IconUtils.cpp b/api/logic/icons/IconUtils.cpp new file mode 100644 index 00000000..bf530c16 --- /dev/null +++ b/api/logic/icons/IconUtils.cpp @@ -0,0 +1,62 @@ +#include "IconUtils.h" + +#include "FileSystem.h" +#include <QDirIterator> + +#include <array> + +namespace { +std::array<const char *, 6> validIconExtensions = {{ + "svg", + "png", + "ico", + "gif", + "jpg", + "jpeg" +}}; +} + +namespace IconUtils{ + +QString findBestIconIn(const QString &folder, const QString & iconKey) { + int best_found = validIconExtensions.size(); + QString best_filename; + + QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags); + while (it.hasNext()) { + it.next(); + auto fileInfo = it.fileInfo(); + + if(fileInfo.completeBaseName() != iconKey) + continue; + + auto extension = fileInfo.suffix(); + + for(int i = 0; i < best_found; i++) { + if(extension == validIconExtensions[i]) { + best_found = i; + qDebug() << i << " : " << fileInfo.fileName(); + best_filename = fileInfo.fileName(); + } + } + } + return FS::PathCombine(folder, best_filename); +} + +QString getIconFilter() { + QString out; + QTextStream stream(&out); + stream << '('; + for(size_t i = 0; i < validIconExtensions.size() - 1; i++) { + if(i > 0) { + stream << " "; + } + stream << "*." << validIconExtensions[i]; + } + stream << " *." << validIconExtensions[validIconExtensions.size() - 1]; + stream << ')'; + return out; +} + +} + diff --git a/api/logic/icons/IconUtils.h b/api/logic/icons/IconUtils.h new file mode 100644 index 00000000..ce236946 --- /dev/null +++ b/api/logic/icons/IconUtils.h @@ -0,0 +1,14 @@ +#pragma once + +#include <QString> +#include "multimc_logic_export.h" + +namespace IconUtils { + +// Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path +MULTIMC_LOGIC_EXPORT QString findBestIconIn(const QString &folder, const QString & iconKey); + +// Get icon file type filter for file browser dialogs +MULTIMC_LOGIC_EXPORT QString getIconFilter(); + +} diff --git a/api/logic/java/JavaChecker.cpp b/api/logic/java/JavaChecker.cpp index 9ba3933f..d78d6505 100644 --- a/api/logic/java/JavaChecker.cpp +++ b/api/logic/java/JavaChecker.cpp @@ -75,8 +75,8 @@ void JavaChecker::stderrReady() void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) { killTimer.stop(); - QProcessPtr _process; - _process.swap(process); + QProcessPtr _process = process; + process.reset(); JavaCheckResult result; { @@ -115,7 +115,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) } } - if(!results.contains("os.arch") || !results.contains("java.version") || !success) + if(!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) { result.validity = JavaCheckResult::Validity::ReturnedInvalidData; emit checkFinished(result); @@ -124,6 +124,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) auto os_arch = results["os.arch"]; auto java_version = results["java.version"]; + auto java_vendor = results["java.vendor"]; bool is_64 = os_arch == "x86_64" || os_arch == "amd64"; @@ -132,6 +133,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) result.mojangPlatform = is_64 ? "64" : "32"; result.realPlatform = os_arch; result.javaVersion = java_version; + result.javaVendor = java_vendor; qDebug() << "Java checker succeeded."; emit checkFinished(result); } diff --git a/api/logic/java/JavaChecker.h b/api/logic/java/JavaChecker.h index d5d4b0de..0a96249a 100644 --- a/api/logic/java/JavaChecker.h +++ b/api/logic/java/JavaChecker.h @@ -3,6 +3,8 @@ #include <QTimer> #include <memory> +#include "QObjectPtr.h" + #include "multimc_logic_export.h" #include "JavaVersion.h" @@ -15,6 +17,7 @@ struct MULTIMC_LOGIC_EXPORT JavaCheckResult QString mojangPlatform; QString realPlatform; JavaVersion javaVersion; + QString javaVendor; QString outLog; QString errorLog; bool is_64bit = false; @@ -27,8 +30,8 @@ struct MULTIMC_LOGIC_EXPORT JavaCheckResult } validity = Validity::Errored; }; -typedef std::shared_ptr<QProcess> QProcessPtr; -typedef std::shared_ptr<JavaChecker> JavaCheckerPtr; +typedef shared_qobject_ptr<QProcess> QProcessPtr; +typedef shared_qobject_ptr<JavaChecker> JavaCheckerPtr; class MULTIMC_LOGIC_EXPORT JavaChecker : public QObject { Q_OBJECT diff --git a/api/logic/java/JavaCheckerJob.cpp b/api/logic/java/JavaCheckerJob.cpp index a26846f2..67d70066 100644 --- a/api/logic/java/JavaCheckerJob.cpp +++ b/api/logic/java/JavaCheckerJob.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/java/JavaCheckerJob.h b/api/logic/java/JavaCheckerJob.h index e52970c1..c0986420 100644 --- a/api/logic/java/JavaCheckerJob.h +++ b/api/logic/java/JavaCheckerJob.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -20,7 +20,7 @@ #include "tasks/Task.h" class JavaCheckerJob; -typedef std::shared_ptr<JavaCheckerJob> JavaCheckerJobPtr; +typedef shared_qobject_ptr<JavaCheckerJob> JavaCheckerJobPtr; // FIXME: this just seems horribly redundant class JavaCheckerJob : public Task diff --git a/api/logic/java/JavaInstallList.cpp b/api/logic/java/JavaInstallList.cpp index 605d4c0c..0bded03c 100644 --- a/api/logic/java/JavaInstallList.cpp +++ b/api/logic/java/JavaInstallList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -149,7 +149,7 @@ void JavaListLoadTask::executeTask() JavaUtils ju; QList<QString> candidate_paths = ju.FindJavaPaths(); - m_job = std::shared_ptr<JavaCheckerJob>(new JavaCheckerJob("Java detection")); + m_job = new JavaCheckerJob("Java detection"); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); diff --git a/api/logic/java/JavaInstallList.h b/api/logic/java/JavaInstallList.h index 7e72b5ef..1785a7b6 100644 --- a/api/logic/java/JavaInstallList.h +++ b/api/logic/java/JavaInstallList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -24,6 +24,8 @@ #include "JavaCheckerJob.h" #include "JavaInstall.h" +#include "QObjectPtr.h" + #include "multimc_logic_export.h" class JavaListLoadTask; @@ -75,7 +77,7 @@ public slots: void javaCheckerFinished(); protected: - std::shared_ptr<JavaCheckerJob> m_job; + shared_qobject_ptr<JavaCheckerJob> m_job; JavaInstallList *m_list; JavaInstall *m_currentRecommended; }; diff --git a/api/logic/java/JavaUtils.cpp b/api/logic/java/JavaUtils.cpp index 9f7fdcb0..4b231e6a 100644 --- a/api/logic/java/JavaUtils.cpp +++ b/api/logic/java/JavaUtils.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -150,7 +150,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava() } #if defined(Q_OS_WIN32) -QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName) +QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) { QList<JavaInstallPtr> javas; @@ -175,8 +175,6 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); } - QString recommended = value; - TCHAR subKeyName[255]; DWORD subKeyNameSize, numSubKeys, retCode; @@ -195,7 +193,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString if (retCode == ERROR_SUCCESS) { // Now open the registry key for the version that we just got. - QString newKeyName = keyName + "\\" + subKeyName; + QString newKeyName = keyName + "\\" + subKeyName + subkeySuffix; HKEY newKey; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0, @@ -204,11 +202,11 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString // Read the JavaHome value to find where Java is installed. value = new char[0]; valueSz = 0; - if (RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + if (RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, &valueSz) == ERROR_MORE_DATA) { value = new char[valueSz]; - RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, &valueSz); // Now, we construct the version object and add it to the list. @@ -237,25 +235,78 @@ QList<QString> JavaUtils::FindJavaPaths() { QList<JavaInstallPtr> java_candidates; + // Oracle QList<JavaInstallPtr> JRE64s = this->FindJavaFromRegistryKey( - KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList<JavaInstallPtr> JDK64s = this->FindJavaFromRegistryKey( - KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); QList<JavaInstallPtr> JRE32s = this->FindJavaFromRegistryKey( - KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList<JavaInstallPtr> JDK32s = this->FindJavaFromRegistryKey( - KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); - + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); + + // Oracle for Java 9 and newer + QList<JavaInstallPtr> NEWJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList<JavaInstallPtr> NEWJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + QList<JavaInstallPtr> NEWJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList<JavaInstallPtr> NEWJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + + // AdoptOpenJDK + QList<JavaInstallPtr> ADOPTOPENJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTOPENJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTOPENJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTOPENJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + + // Microsoft + QList<JavaInstallPtr> MICROSOFTJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); + + // Azul Zulu + QList<JavaInstallPtr> ZULU64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + QList<JavaInstallPtr> ZULU32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + + // BellSoft Liberica + QList<JavaInstallPtr> LIBERICA64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + QList<JavaInstallPtr> LIBERICA32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + + // List x64 before x86 java_candidates.append(JRE64s); + java_candidates.append(NEWJRE64s); + java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK64s); + java_candidates.append(NEWJDK64s); + java_candidates.append(ADOPTOPENJDK64s); + java_candidates.append(MICROSOFTJDK64s); + java_candidates.append(ZULU64s); + java_candidates.append(LIBERICA64s); + java_candidates.append(JRE32s); + java_candidates.append(NEWJRE32s); + java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK32s); + java_candidates.append(NEWJDK32s); + java_candidates.append(ADOPTOPENJDK32s); + java_candidates.append(ZULU32s); + java_candidates.append(LIBERICA32s); + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); QList<QString> candidates; @@ -330,6 +381,9 @@ QList<QString> JavaUtils::FindJavaPaths() scanJavaDir("/usr/lib32/jvm"); // javas stored in MultiMC's folder scanJavaDir("java"); + // manually installed JDKs in /opt + scanJavaDir("/opt/jdk"); + scanJavaDir("/opt/jdks"); return javas; } #else diff --git a/api/logic/java/JavaUtils.h b/api/logic/java/JavaUtils.h index 40745ad6..206acf89 100644 --- a/api/logic/java/JavaUtils.h +++ b/api/logic/java/JavaUtils.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -39,6 +39,6 @@ public: JavaInstallPtr GetDefaultJava(); #ifdef Q_OS_WIN - QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName); + QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); #endif }; diff --git a/api/logic/java/launch/CheckJava.cpp b/api/logic/java/launch/CheckJava.cpp index 4ea40084..f58602f0 100644 --- a/api/logic/java/launch/CheckJava.cpp +++ b/api/logic/java/launch/CheckJava.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -33,17 +33,17 @@ void CheckJava::executeTask() if (perInstance) { emit logLine( - tr("The java binary \"%1\" couldn't be found. Please fix the java path " + QString("The java binary \"%1\" couldn't be found. Please fix the java path " "override in the instance's settings or disable it.").arg(m_javaPath), MessageLevel::Warning); } else { - emit logLine(tr("The java binary \"%1\" couldn't be found. Please set up java in " + emit logLine(QString("The java binary \"%1\" couldn't be found. Please set up java in " "the settings.").arg(m_javaPath), MessageLevel::Warning); } - emitFailed(tr("Java path is not valid.")); + emitFailed(QString("Java path is not valid.")); return; } else @@ -56,12 +56,13 @@ void CheckJava::executeTask() auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); + auto storedVendor = settings->get("JavaVendor").toString(); m_javaUnixTime = javaUnixTime; // if timestamps are not the same, or something is missing, check! - if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0) + if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedVendor.size() == 0) { - m_JavaChecker = std::make_shared<JavaChecker>(); - emit logLine(tr("Checking Java version..."), MessageLevel::MultiMC); + m_JavaChecker = new JavaChecker(); + emit logLine(QString("Checking Java version..."), MessageLevel::MultiMC); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->m_path = realJavaPath; m_JavaChecker->performCheck(); @@ -71,7 +72,8 @@ void CheckJava::executeTask() { auto verString = instance->settings()->get("JavaVersion").toString(); auto archString = instance->settings()->get("JavaArchitecture").toString(); - printJavaInfo(verString, archString); + auto vendorString = instance->settings()->get("JavaVendor").toString(); + printJavaInfo(verString, archString, vendorString); } emitSucceeded(); } @@ -83,16 +85,16 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) case JavaCheckResult::Validity::Errored: { // Error message displayed if java can't start - emit logLine(tr("Could not start java:"), MessageLevel::Error); + emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC); printSystemInfo(false, false); - emitFailed(tr("Could not start java!")); + emitFailed(QString("Could not start java!")); return; } case JavaCheckResult::Validity::ReturnedInvalidData: { - emit logLine(tr("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error); + emit logLine(QString("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::MultiMC); printSystemInfo(false, false); @@ -102,9 +104,10 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) case JavaCheckResult::Validity::Valid: { auto instance = m_parent->instance(); - printJavaInfo(result.javaVersion.toString(), result.mojangPlatform); + printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.javaVendor); instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaArchitecture", result.mojangPlatform); + instance->settings()->set("JavaVendor", result.javaVendor); instance->settings()->set("JavaTimestamp", m_javaUnixTime); emitSucceeded(); return; @@ -112,9 +115,9 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) } } -void CheckJava::printJavaInfo(const QString& version, const QString& architecture) +void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor) { - emit logLine(tr("Java is version %1, using %2-bit architecture.\n\n").arg(version, architecture), MessageLevel::MultiMC); + emit logLine(QString("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::MultiMC); printSystemInfo(true, architecture == "64"); } @@ -124,13 +127,13 @@ void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit) auto system64 = Sys::isSystem64bit(); if(cpu64 != system64) { - emit logLine(tr("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error); + emit logLine(QString("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error); } if(javaIsKnown) { if(javaIs64bit != system64) { - emit logLine(tr("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error); + emit logLine(QString("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error); } } } diff --git a/api/logic/java/launch/CheckJava.h b/api/logic/java/launch/CheckJava.h index be247a1b..68cd618b 100644 --- a/api/logic/java/launch/CheckJava.h +++ b/api/logic/java/launch/CheckJava.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -35,7 +35,7 @@ private slots: void checkJavaFinished(JavaCheckResult result); private: - void printJavaInfo(const QString & version, const QString & architecture); + void printJavaInfo(const QString & version, const QString & architecture, const QString & vendor); void printSystemInfo(bool javaIsKnown, bool javaIs64bit); private: diff --git a/api/logic/launch/LaunchStep.cpp b/api/logic/launch/LaunchStep.cpp index e2327712..d6bb6e88 100644 --- a/api/logic/launch/LaunchStep.cpp +++ b/api/logic/launch/LaunchStep.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/LaunchStep.h b/api/logic/launch/LaunchStep.h index 1aaf64af..3939f960 100644 --- a/api/logic/launch/LaunchStep.h +++ b/api/logic/launch/LaunchStep.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/LaunchTask.cpp b/api/logic/launch/LaunchTask.cpp index c00fa32a..e6f6bbac 100644 --- a/api/logic/launch/LaunchTask.cpp +++ b/api/logic/launch/LaunchTask.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * @@ -33,9 +33,9 @@ void LaunchTask::init() m_instance->setRunning(true); } -std::shared_ptr<LaunchTask> LaunchTask::create(InstancePtr inst) +shared_qobject_ptr<LaunchTask> LaunchTask::create(InstancePtr inst) { - std::shared_ptr<LaunchTask> proc(new LaunchTask(inst)); + shared_qobject_ptr<LaunchTask> proc(new LaunchTask(inst)); proc->init(); return proc; } @@ -44,12 +44,12 @@ LaunchTask::LaunchTask(InstancePtr instance): m_instance(instance) { } -void LaunchTask::appendStep(std::shared_ptr<LaunchStep> step) +void LaunchTask::appendStep(shared_qobject_ptr<LaunchStep> step) { m_steps.append(step); } -void LaunchTask::prependStep(std::shared_ptr<LaunchStep> step) +void LaunchTask::prependStep(shared_qobject_ptr<LaunchStep> step) { m_steps.prepend(step); } diff --git a/api/logic/launch/LaunchTask.h b/api/logic/launch/LaunchTask.h index f5e226b5..2be59c83 100644 --- a/api/logic/launch/LaunchTask.h +++ b/api/logic/launch/LaunchTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * @@ -45,11 +45,11 @@ public: }; public: /* methods */ - static std::shared_ptr<LaunchTask> create(InstancePtr inst); + static shared_qobject_ptr<LaunchTask> create(InstancePtr inst); virtual ~LaunchTask() {}; - void appendStep(std::shared_ptr<LaunchStep> step); - void prependStep(std::shared_ptr<LaunchStep> step); + void appendStep(shared_qobject_ptr<LaunchStep> step); + void prependStep(shared_qobject_ptr<LaunchStep> step); void setCensorFilter(QMap<QString, QString> filter); InstancePtr instance() @@ -117,7 +117,7 @@ private: /*methods */ protected: /* data */ InstancePtr m_instance; shared_qobject_ptr<LogModel> m_logModel; - QList <std::shared_ptr<LaunchStep>> m_steps; + QList <shared_qobject_ptr<LaunchStep>> m_steps; QMap<QString, QString> m_censorFilter; int currentStep = -1; State state = NotStarted; diff --git a/api/logic/launch/steps/LookupServerAddress.cpp b/api/logic/launch/steps/LookupServerAddress.cpp new file mode 100644 index 00000000..de56c28a --- /dev/null +++ b/api/logic/launch/steps/LookupServerAddress.cpp @@ -0,0 +1,95 @@ +/* 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 "LookupServerAddress.h" + +#include <launch/LaunchTask.h> + +LookupServerAddress::LookupServerAddress(LaunchTask *parent) : + LaunchStep(parent), m_dnsLookup(new QDnsLookup(this)) +{ + connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished); + + m_dnsLookup->setType(QDnsLookup::SRV); +} + +void LookupServerAddress::setLookupAddress(const QString &lookupAddress) +{ + m_lookupAddress = lookupAddress; + m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); +} + +void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output) +{ + m_output = std::move(output); +} + +bool LookupServerAddress::abort() +{ + m_dnsLookup->abort(); + emitFailed("Aborted"); + return true; +} + +void LookupServerAddress::executeTask() +{ + m_dnsLookup->lookup(); +} + +void LookupServerAddress::on_dnsLookupFinished() +{ + if (isFinished()) + { + // Aborted + return; + } + + if (m_dnsLookup->error() != QDnsLookup::NoError) + { + emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") + .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::MultiMC); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto records = m_dnsLookup->serviceRecords(); + if (records.empty()) + { + emit logLine( + QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") + .arg(m_dnsLookup->name()), MessageLevel::Warning); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto &firstRecord = records.at(0); + quint16 port = firstRecord.port(); + + emit logLine(QString("Resolved server address %1 to %2 with port %3\n").arg( + m_dnsLookup->name(), firstRecord.target(), QString::number(port)),MessageLevel::MultiMC); + resolve(firstRecord.target(), port); +} + +void LookupServerAddress::resolve(const QString &address, quint16 port) +{ + m_output->address = address; + m_output->port = port; + + emitSucceeded(); + m_dnsLookup->deleteLater(); +} diff --git a/api/logic/launch/steps/LookupServerAddress.h b/api/logic/launch/steps/LookupServerAddress.h new file mode 100644 index 00000000..5a5c3de1 --- /dev/null +++ b/api/logic/launch/steps/LookupServerAddress.h @@ -0,0 +1,49 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <launch/LaunchStep.h> +#include <QObjectPtr.h> +#include <QDnsLookup> + +#include "minecraft/launch/MinecraftServerTarget.h" + +class LookupServerAddress: public LaunchStep { +Q_OBJECT +public: + explicit LookupServerAddress(LaunchTask *parent); + virtual ~LookupServerAddress() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + + void setLookupAddress(const QString &lookupAddress); + void setOutputAddressPtr(MinecraftServerTargetPtr output); + +private slots: + void on_dnsLookupFinished(); + +private: + void resolve(const QString &address, quint16 port); + + QDnsLookup *m_dnsLookup; + QString m_lookupAddress; + MinecraftServerTargetPtr m_output; +}; diff --git a/api/logic/launch/steps/PostLaunchCommand.cpp b/api/logic/launch/steps/PostLaunchCommand.cpp index 3730f71c..d48d03d1 100644 --- a/api/logic/launch/steps/PostLaunchCommand.cpp +++ b/api/logic/launch/steps/PostLaunchCommand.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/steps/PostLaunchCommand.h b/api/logic/launch/steps/PostLaunchCommand.h index efba62d9..ab4c494f 100644 --- a/api/logic/launch/steps/PostLaunchCommand.h +++ b/api/logic/launch/steps/PostLaunchCommand.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/steps/PreLaunchCommand.cpp b/api/logic/launch/steps/PreLaunchCommand.cpp index c5e1e373..20e089e2 100644 --- a/api/logic/launch/steps/PreLaunchCommand.cpp +++ b/api/logic/launch/steps/PreLaunchCommand.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/steps/PreLaunchCommand.h b/api/logic/launch/steps/PreLaunchCommand.h index 861e5be7..dc069f71 100644 --- a/api/logic/launch/steps/PreLaunchCommand.h +++ b/api/logic/launch/steps/PreLaunchCommand.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/steps/TextPrint.h b/api/logic/launch/steps/TextPrint.h index 48b8cf6f..2937c64a 100644 --- a/api/logic/launch/steps/TextPrint.h +++ b/api/logic/launch/steps/TextPrint.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/launch/steps/Update.cpp b/api/logic/launch/steps/Update.cpp index 65f24391..28bd153d 100644 --- a/api/logic/launch/steps/Update.cpp +++ b/api/logic/launch/steps/Update.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -49,7 +49,7 @@ void Update::updateFinished() } else { - QString reason = tr("Instance update failed because: %1.\n\n").arg(m_updateTask->failReason()); + QString reason = tr("Instance update failed because: %1\n\n").arg(m_updateTask->failReason()); m_updateTask.reset(); emit logLine(reason, MessageLevel::Fatal); emitFailed(reason); diff --git a/api/logic/launch/steps/Update.h b/api/logic/launch/steps/Update.h index baa0b80e..0c9d91e0 100644 --- a/api/logic/launch/steps/Update.h +++ b/api/logic/launch/steps/Update.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/meta/BaseEntity.cpp b/api/logic/meta/BaseEntity.cpp index 9ea712fa..5ff7a59a 100644 --- a/api/logic/meta/BaseEntity.cpp +++ b/api/logic/meta/BaseEntity.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -24,6 +24,8 @@ #include "Env.h" #include "Json.h" +#include "BuildConfig.h" + class ParsingValidator : public Net::Validator { public: /* con/des */ @@ -53,7 +55,9 @@ public: /* methods */ auto fname = m_entity->localFilename(); try { - m_entity->parse(Json::requireObject(Json::requireDocument(data, fname), fname)); + auto doc = Json::requireDocument(data, fname); + auto obj = Json::requireObject(doc, fname); + m_entity->parse(obj); return true; } catch (const Exception &e) @@ -74,7 +78,7 @@ Meta::BaseEntity::~BaseEntity() QUrl Meta::BaseEntity::url() const { - return QUrl("https://v1.meta.multimc.org").resolved(localFilename()); + return QUrl(BuildConfig.META_URL).resolved(localFilename()); } bool Meta::BaseEntity::loadLocalFile() @@ -87,7 +91,9 @@ bool Meta::BaseEntity::loadLocalFile() // TODO: check if the file has the expected checksum try { - parse(Json::requireObject(Json::requireDocument(fname, fname), fname)); + auto doc = Json::requireDocument(fname, fname); + auto obj = Json::requireObject(doc, fname); + parse(obj); return true; } catch (const Exception &e) diff --git a/api/logic/meta/BaseEntity.h b/api/logic/meta/BaseEntity.h index 19dd2c18..04a37420 100644 --- a/api/logic/meta/BaseEntity.h +++ b/api/logic/meta/BaseEntity.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/Index.cpp b/api/logic/meta/Index.cpp index 4dbc0a86..6802470d 100644 --- a/api/logic/meta/Index.cpp +++ b/api/logic/meta/Index.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/Index.h b/api/logic/meta/Index.h index c81b4c54..e9412e70 100644 --- a/api/logic/meta/Index.h +++ b/api/logic/meta/Index.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/JsonFormat.cpp b/api/logic/meta/JsonFormat.cpp index 12da266f..796da4bb 100644 --- a/api/logic/meta/JsonFormat.cpp +++ b/api/logic/meta/JsonFormat.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/JsonFormat.h b/api/logic/meta/JsonFormat.h index 62351ad6..93217b7e 100644 --- a/api/logic/meta/JsonFormat.h +++ b/api/logic/meta/JsonFormat.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/Version.cpp b/api/logic/meta/Version.cpp index 05a76b41..a8dc3169 100644 --- a/api/logic/meta/Version.cpp +++ b/api/logic/meta/Version.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -18,7 +18,7 @@ #include <QDateTime> #include "JsonFormat.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" Meta::Version::Version(const QString &uid, const QString &version) : BaseVersion(), m_uid(uid), m_version(version) diff --git a/api/logic/meta/Version.h b/api/logic/meta/Version.h index 6506d486..a38d7bcd 100644 --- a/api/logic/meta/Version.h +++ b/api/logic/meta/Version.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/VersionList.cpp b/api/logic/meta/VersionList.cpp index 526fe165..607007eb 100644 --- a/api/logic/meta/VersionList.cpp +++ b/api/logic/meta/VersionList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/meta/VersionList.h b/api/logic/meta/VersionList.h index 0be074dd..bba32ca3 100644 --- a/api/logic/meta/VersionList.h +++ b/api/logic/meta/VersionList.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. diff --git a/api/logic/minecraft/AssetsUtils.cpp b/api/logic/minecraft/AssetsUtils.cpp index c6db2a40..c01733b6 100644 --- a/api/logic/minecraft/AssetsUtils.cpp +++ b/api/logic/minecraft/AssetsUtils.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -27,6 +27,34 @@ #include "FileSystem.h" #include "net/Download.h" #include "net/ChecksumValidator.h" +#include "BuildConfig.h" + +namespace { +QSet<QString> collectPathsFromDir(QString dirPath) +{ + QFileInfo dirInfo(dirPath); + + if (!dirInfo.exists()) + { + return {}; + } + + QSet<QString> out; + + QDirIterator iter(dirPath, QDirIterator::Subdirectories); + while (iter.hasNext()) + { + QString value = iter.next(); + QFileInfo info(value); + if(info.isFile()) + { + out.insert(value); + qDebug() << value; + } + } + return out; +} +} namespace AssetsUtils @@ -36,7 +64,7 @@ namespace AssetsUtils * Returns true on success, with index populated * index is undefined otherwise */ -bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) +bool loadAssetsIndexJson(const QString &assetsId, const QString &path, AssetsIndex& index) { /* { @@ -60,7 +88,7 @@ bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) qCritical() << "Failed to read assets index file" << path; return false; } - index->id = assetsId; + index.id = assetsId; // Read the file and close it. QByteArray jsonData = file.readAll(); @@ -89,7 +117,13 @@ bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) QJsonValue isVirtual = root.value("virtual"); if (!isVirtual.isUndefined()) { - index->isVirtual = isVirtual.toBool(false); + index.isVirtual = isVirtual.toBool(false); + } + + QJsonValue mapToResources = root.value("map_to_resources"); + if (!mapToResources.isUndefined()) + { + index.mapToResources = mapToResources.toBool(false); } QJsonValue objects = root.value("objects"); @@ -121,13 +155,14 @@ bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) } } - index->objects.insert(iter.key(), object); + index.objects.insert(iter.key(), object); } return true; } -QDir reconstructAssets(QString assetsId) +// FIXME: ugly code duplication +QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder) { QDir assetsDir = QDir("assets/"); QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); @@ -140,24 +175,77 @@ QDir reconstructAssets(QString assetsId) if (!indexFile.exists()) { - qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets"; + qCritical() << "No assets index file" << indexPath << "; can't determine assets path!"; return virtualRoot; } - qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() - << objectDir.path() << virtualDir.path() << virtualRoot.path(); + AssetsIndex index; + if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!"; + return virtualRoot; + } + + QString targetPath; + if(index.isVirtual) + { + return virtualRoot; + } + else if(index.mapToResources) + { + return QDir(resourcesFolder); + } + return virtualRoot; +} + +// FIXME: ugly code duplication +bool reconstructAssets(QString assetsId, QString resourcesFolder) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!"; + return false; + } + + qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path(); AssetsIndex index; - bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, &index); + if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!"; + return false; + } - if (loadAssetsIndex && index.isVirtual) + QString targetPath; + bool removeLeftovers = false; + if(index.isVirtual) + { + targetPath = virtualRoot.path(); + removeLeftovers = true; + qDebug() << "Reconstructing virtual assets folder at" << targetPath; + } + else if(index.mapToResources) { - qDebug() << "Reconstructing virtual assets folder at" << virtualRoot.path(); + targetPath = resourcesFolder; + qDebug() << "Reconstructing resources folder at" << targetPath; + } + if (!targetPath.isNull()) + { + auto presentFiles = collectPathsFromDir(targetPath); for (QString map : index.objects.keys()) { AssetObject asset_object = index.objects.value(map); - QString target_path = FS::PathCombine(virtualRoot.path(), map); + QString target_path = FS::PathCombine(targetPath, map); QFile target(target_path); QString tlk = asset_object.hash.left(2); @@ -166,24 +254,32 @@ QDir reconstructAssets(QString assetsId) QFile original(original_path); if (!original.exists()) continue; + + presentFiles.remove(target_path); + if (!target.exists()) { QFileInfo info(target_path); QDir target_dir = info.dir(); - // qDebug() << target_dir; - if (!target_dir.exists()) - QDir("").mkpath(target_dir.path()); + + qDebug() << target_dir.path(); + FS::ensureFolderPathExists(target_dir.path()); bool couldCopy = original.copy(target_path); - qDebug() << " Copying" << original_path << "to" << target_path - << QString::number(couldCopy); // << original.errorString(); + qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy); } } // TODO: Write last used time to virtualRoot/.lastused + if(removeLeftovers) + { + for(auto & file: presentFiles) + { + qDebug() << "Would remove" << file; + } + } } - - return virtualRoot; + return true; } } @@ -212,7 +308,7 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - return QUrl("https://resources.download.minecraft.net/" + getRelPath()); + return BuildConfig.RESOURCE_BASE + getRelPath(); } QString AssetObject::getRelPath() diff --git a/api/logic/minecraft/AssetsUtils.h b/api/logic/minecraft/AssetsUtils.h index 063c1237..32e57060 100644 --- a/api/logic/minecraft/AssetsUtils.h +++ b/api/logic/minecraft/AssetsUtils.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -38,11 +38,16 @@ struct AssetsIndex QString id; QMap<QString, AssetObject> objects; bool isVirtual = false; + bool mapToResources = false; }; +/// FIXME: this is absolutely horrendous. REDO!!!! namespace AssetsUtils { -bool loadAssetsIndexJson(QString id, QString file, AssetsIndex* index); +bool loadAssetsIndexJson(const QString &id, const QString &file, AssetsIndex& index); + +QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder); + /// Reconstruct a virtual assets folder for the given assets ID and return the folder -QDir reconstructAssets(QString assetsId); +bool reconstructAssets(QString assetsId, QString resourcesFolder); } diff --git a/api/logic/minecraft/Component.cpp b/api/logic/minecraft/Component.cpp index 51957d17..92821065 100644 --- a/api/logic/minecraft/Component.cpp +++ b/api/logic/minecraft/Component.cpp @@ -5,13 +5,13 @@ #include "meta/Version.h" #include "VersionFile.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include <FileSystem.h> #include <QSaveFile> #include "OneSixVersionFormat.h" #include <assert.h> -Component::Component(ComponentList * parent, const QString& uid) +Component::Component(PackProfile * parent, const QString& uid) { assert(parent); m_parent = parent; @@ -19,7 +19,7 @@ Component::Component(ComponentList * parent, const QString& uid) m_uid = uid; } -Component::Component(ComponentList * parent, std::shared_ptr<Meta::Version> version) +Component::Component(PackProfile * parent, std::shared_ptr<Meta::Version> version) { assert(parent); m_parent = parent; @@ -31,7 +31,7 @@ Component::Component(ComponentList * parent, std::shared_ptr<Meta::Version> vers m_loaded = version->isLoaded(); } -Component::Component(ComponentList * parent, const QString& uid, std::shared_ptr<VersionFile> file) +Component::Component(PackProfile * parent, const QString& uid, std::shared_ptr<VersionFile> file) { assert(parent); m_parent = parent; diff --git a/api/logic/minecraft/Component.h b/api/logic/minecraft/Component.h index 6a0f86c8..cb202f7f 100644 --- a/api/logic/minecraft/Component.h +++ b/api/logic/minecraft/Component.h @@ -9,7 +9,7 @@ #include "QObjectPtr.h" #include "multimc_logic_export.h" -class ComponentList; +class PackProfile; class LaunchProfile; namespace Meta { @@ -22,11 +22,11 @@ class MULTIMC_LOGIC_EXPORT Component : public QObject, public ProblemProvider { Q_OBJECT public: - Component(ComponentList * parent, const QString &uid); + Component(PackProfile * parent, const QString &uid); // DEPRECATED: remove these constructors? - Component(ComponentList * parent, std::shared_ptr<Meta::Version> version); - Component(ComponentList * parent, const QString & uid, std::shared_ptr<VersionFile> file); + Component(PackProfile * parent, std::shared_ptr<Meta::Version> version); + Component(PackProfile * parent, const QString & uid, std::shared_ptr<VersionFile> file); virtual ~Component(){}; void applyTo(LaunchProfile *profile); @@ -73,7 +73,7 @@ signals: void dataChanged(); public: /* data */ - ComponentList * m_parent; + PackProfile * m_parent; // BEGIN: persistent component list properties /// ID of the component diff --git a/api/logic/minecraft/ComponentUpdateTask.cpp b/api/logic/minecraft/ComponentUpdateTask.cpp index 37cc488d..241d9a49 100644 --- a/api/logic/minecraft/ComponentUpdateTask.cpp +++ b/api/logic/minecraft/ComponentUpdateTask.cpp @@ -1,7 +1,7 @@ #include "ComponentUpdateTask.h" -#include "ComponentList_p.h" -#include "ComponentList.h" +#include "PackProfile_p.h" +#include "PackProfile.h" #include "Component.h" #include <Env.h> #include <meta/Index.h> @@ -22,16 +22,16 @@ * Really, it should be a reactor/state machine that receives input from the application * and dynamically adapts to changing requirements... * - * The reactor should be the only entry into manipulating the ComponentList. + * The reactor should be the only entry into manipulating the PackProfile. * See: https://en.wikipedia.org/wiki/Reactor_pattern */ /* - * Or make this operate on a snapshot of the ComponentList state, then merge results in as long as the snapshot and ComponentList didn't change? + * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change? * If the component list changes, start over. */ -ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent) +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) : Task(parent) { d.reset(new ComponentUpdateTaskData); @@ -126,7 +126,7 @@ static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task> // FIXME: dead code. determine if this can still be useful? /* -static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) +static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) { if(component->m_loaded) { @@ -217,7 +217,7 @@ void ComponentUpdateTask::loadComponents() } case Mode::Resolution: { - singleResult = loadComponentList(component, loadTask, d->netmode); + singleResult = loadPackProfile(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::List; break; } @@ -244,7 +244,7 @@ void ComponentUpdateTask::loadComponents() }); RemoteLoadStatus status; status.type = loadType; - status.componentListIndex = componentIndex; + status.PackProfileIndex = componentIndex; d->remoteLoadStatusList.append(status); taskIndex++; } @@ -451,13 +451,17 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi auto & comp = (*compIter); if(comp->getVersion() != req.equalsVersion) { - if(comp->m_dependencyOnly) - { - decision = Decision::VersionNotSame; - } - else - { + if(comp->isCustom()) { decision = Decision::LockedVersionNotSame; + } else { + if(comp->m_dependencyOnly) + { + decision = Decision::VersionNotSame; + } + else + { + decision = Decision::LockedVersionNotSame; + } } break; } @@ -491,7 +495,7 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi } // FIXME, TODO: decouple dependency resolution from loading -// FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses. +// FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { @@ -586,6 +590,15 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) { component->m_version = "3.1.2"; } + else if (add.uid == "net.fabricmc.intermediary") + { + auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ + return cmp->getID() == "net.minecraft"; + }); + if(minecraft != components.end()) { + component->m_version = (*minecraft)->getVersion(); + } + } } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ @@ -635,7 +648,7 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { - auto component = d->m_list->getComponent(taskSlot.componentListIndex); + auto component = d->m_list->getComponent(taskSlot.PackProfileIndex); component->m_loaded = true; component->updateCachedData(); } diff --git a/api/logic/minecraft/ComponentUpdateTask.h b/api/logic/minecraft/ComponentUpdateTask.h index 4cb29a89..4274cabb 100644 --- a/api/logic/minecraft/ComponentUpdateTask.h +++ b/api/logic/minecraft/ComponentUpdateTask.h @@ -4,7 +4,7 @@ #include "net/Mode.h" #include <memory> -class ComponentList; +class PackProfile; struct ComponentUpdateTaskData; class ComponentUpdateTask : public Task @@ -18,7 +18,7 @@ public: }; public: - explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList * list, QObject *parent = 0); + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile * list, QObject *parent = 0); virtual ~ComponentUpdateTask(); protected: diff --git a/api/logic/minecraft/ComponentUpdateTask_p.h b/api/logic/minecraft/ComponentUpdateTask_p.h index 5989cf07..5b02431b 100644 --- a/api/logic/minecraft/ComponentUpdateTask_p.h +++ b/api/logic/minecraft/ComponentUpdateTask_p.h @@ -5,7 +5,7 @@ #include <QList> #include "net/Mode.h" -class ComponentList; +class PackProfile; struct RemoteLoadStatus { @@ -15,7 +15,7 @@ struct RemoteLoadStatus List, Version } type = Type::Version; - size_t componentListIndex = 0; + size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; QString error; @@ -23,7 +23,7 @@ struct RemoteLoadStatus struct ComponentUpdateTaskData { - ComponentList * m_list = nullptr; + PackProfile * m_list = nullptr; QList<RemoteLoadStatus> remoteLoadStatusList; bool remoteLoadSuccessful = true; size_t remoteTasksInProgress = 0; diff --git a/api/logic/minecraft/GradleSpecifier.h b/api/logic/minecraft/GradleSpecifier.h index 959325c6..60e0a726 100644 --- a/api/logic/minecraft/GradleSpecifier.h +++ b/api/logic/minecraft/GradleSpecifier.h @@ -18,32 +18,35 @@ struct GradleSpecifier { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar - DEBUG 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" - DEBUG 1 "org.gradle.test.classifiers" - DEBUG 2 "service" - DEBUG 3 "1.0" - DEBUG 4 ":jdk15" - DEBUG 5 "jdk15" - DEBUG 6 "@jar" - DEBUG 7 "jar" + 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + 1 "org.gradle.test.classifiers" + 2 "service" + 3 "1.0" + 4 "jdk15" + 5 "jar" */ - QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(:([^:@]+))?" "(@([^:@]+))?"); + QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?"); m_valid = matcher.exactMatch(value); + if(!m_valid) { + m_invalidValue = value; + return *this; + } auto elements = matcher.capturedTexts(); m_groupId = elements[1]; m_artifactId = elements[2]; m_version = elements[3]; - m_classifier = elements[5]; - if(!elements[7].isEmpty()) + m_classifier = elements[4]; + if(!elements[5].isEmpty()) { - m_extension = elements[7]; + m_extension = elements[5]; } return *this; } - operator QString() const + QString serialize() const { - if(!m_valid) - return "INVALID"; + if(!m_valid) { + return m_invalidValue; + } QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; if(!m_classifier.isEmpty()) { @@ -57,6 +60,9 @@ struct GradleSpecifier } QString getFileName() const { + if(!m_valid) { + return QString(); + } QString filename = m_artifactId + '-' + m_version; if(!m_classifier.isEmpty()) { @@ -67,8 +73,9 @@ struct GradleSpecifier } QString toPath(const QString & filenameOverride = QString()) const { - if(!m_valid) - return "INVALID"; + if(!m_valid) { + return QString(); + } QString filename; if(filenameOverride.isEmpty()) { @@ -134,6 +141,7 @@ struct GradleSpecifier return true; } private: + QString m_invalidValue; QString m_groupId; QString m_artifactId; QString m_version; diff --git a/api/logic/minecraft/GradleSpecifier_test.cpp b/api/logic/minecraft/GradleSpecifier_test.cpp index f49ec718..0900c9d8 100644 --- a/api/logic/minecraft/GradleSpecifier_test.cpp +++ b/api/logic/minecraft/GradleSpecifier_test.cpp @@ -31,7 +31,7 @@ slots: { QFETCH(QString, through); - QString converted = GradleSpecifier(through); + QString converted = GradleSpecifier(through).serialize(); QCOMPARE(converted, through); } @@ -68,7 +68,8 @@ slots: GradleSpecifier spec(input); QVERIFY(!spec.valid()); - QCOMPARE(spec.operator QString(), QString("INVALID")); + QCOMPARE(spec.serialize(), input); + QCOMPARE(spec.toPath(), QString()); } }; diff --git a/api/logic/minecraft/LaunchProfile.cpp b/api/logic/minecraft/LaunchProfile.cpp index c39bdf04..41705187 100644 --- a/api/logic/minecraft/LaunchProfile.cpp +++ b/api/logic/minecraft/LaunchProfile.cpp @@ -11,6 +11,7 @@ void LaunchProfile::clear() m_mainClass.clear(); m_appletClass.clear(); m_libraries.clear(); + m_mavenFiles.clear(); m_traits.clear(); m_jarMods.clear(); m_mainJar.reset(); @@ -157,6 +158,22 @@ void LaunchProfile::applyLibrary(LibraryPtr library) } } +void LaunchProfile::applyMavenFile(LibraryPtr mavenFile) +{ + if(!mavenFile->isActive()) + { + return; + } + + if(mavenFile->isNative()) + { + return; + } + + // unlike libraries, we do not keep only one version or try to dedupe them + m_mavenFiles.append(Library::limitedCopy(mavenFile)); +} + const LibraryPtr LaunchProfile::getMainJar() const { return m_mainJar; @@ -253,6 +270,11 @@ const QList<LibraryPtr> & LaunchProfile::getNativeLibraries() const return m_nativeLibraries; } +const QList<LibraryPtr> & LaunchProfile::getMavenFiles() const +{ + return m_mavenFiles; +} + void LaunchProfile::getLibraryFiles( const QString& architecture, QStringList& jars, diff --git a/api/logic/minecraft/LaunchProfile.h b/api/logic/minecraft/LaunchProfile.h index 77174079..c1752531 100644 --- a/api/logic/minecraft/LaunchProfile.h +++ b/api/logic/minecraft/LaunchProfile.h @@ -20,6 +20,7 @@ public: /* application of profile variables from patches */ void applyJarMods(const QList<LibraryPtr> &jarMods); void applyMods(const QList<LibraryPtr> &jarMods); void applyLibrary(LibraryPtr library); + void applyMavenFile(LibraryPtr library); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile @@ -37,6 +38,7 @@ public: /* getters for profile variables */ const QList<LibraryPtr> & getJarMods() const; const QList<LibraryPtr> & getLibraries() const; const QList<LibraryPtr> & getNativeLibraries() const; + const QList<LibraryPtr> & getMavenFiles() const; const LibraryPtr getMainJar() const; void getLibraryFiles( const QString & architecture, @@ -79,10 +81,13 @@ private: /// the list of libraries QList<LibraryPtr> m_libraries; + /// the list of maven files to be placed in the libraries folder, but not acted upon + QList<LibraryPtr> m_mavenFiles; + /// the main jar LibraryPtr m_mainJar; - /// the list of libraries + /// the list of native libraries QList<LibraryPtr> m_nativeLibraries; /// traits, collected from all the version files (version files can only add) diff --git a/api/logic/minecraft/Library.cpp b/api/logic/minecraft/Library.cpp index a6ec0301..f2293679 100644 --- a/api/logic/minecraft/Library.cpp +++ b/api/logic/minecraft/Library.cpp @@ -3,9 +3,9 @@ #include <net/Download.h> #include <net/ChecksumValidator.h> -#include <minecraft/forge/ForgeXzDownload.h> #include <Env.h> #include <FileSystem.h> +#include <BuildConfig.h> void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, @@ -18,13 +18,7 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na if(local && !overridePath.isEmpty()) { QString fileName = out.fileName(); - auto fullPath = FS::PathCombine(overridePath, fileName); - qDebug() << fullPath; - QFileInfo fileinfo(fullPath); - if(fileinfo.exists()) - { - return fileinfo.absoluteFilePath(); - } + return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); } return out.absoluteFilePath(); }; @@ -51,77 +45,62 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na } } -QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class HttpMetaCache* cache, - QStringList& failedFiles, const QString & overridePath) const +QList< std::shared_ptr< NetAction > > Library::getDownloads( + OpSys system, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString & overridePath +) const { QList<NetActionPtr> out; - bool isAlwaysStale = (hint() == "always-stale"); + bool stale = isAlwaysStale(); bool local = isLocal(); - bool isForge = (hint() == "forge-pack-xz"); + + auto check_local_file = [&](QString storage) + { + QFileInfo fileinfo(storage); + QString fileName = fileinfo.fileName(); + auto fullPath = FS::PathCombine(overridePath, fileName); + QFileInfo localFileInfo(fullPath); + if(!localFileInfo.exists()) + { + failedLocalFiles.append(localFileInfo.filePath()); + return false; + } + return true; + }; auto add_download = [&](QString storage, QString url, QString sha1) { + if(local) + { + return check_local_file(storage); + } auto entry = cache->resolveEntry("libraries", storage); - if(isAlwaysStale) + if(stale) { entry->setStale(true); } if (!entry->isStale()) return true; - if(local) - { - if(!overridePath.isEmpty()) - { - QString fileName; - int position = storage.lastIndexOf('/'); - if(position == -1) - { - fileName = storage; - } - else - { - fileName = storage.mid(position); - } - auto fullPath = FS::PathCombine(overridePath, fileName); - QFileInfo fileinfo(fullPath); - if(fileinfo.exists()) - { - return true; - } - } - QFileInfo fileinfo(entry->getFullPath()); - if(!fileinfo.exists()) - { - failedFiles.append(entry->getFullPath()); - return false; - } - return true; - } Net::Download::Options options; - if(isAlwaysStale) + if(stale) { options |= Net::Download::Option::AcceptLocalFiles; } - if (isForge) + + if(sha1.size()) { - qDebug() << "XzDownload for:" << rawName() << "storage:" << storage << "url:" << url; - out.append(ForgeXzDownload::make(storage, entry)); + auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); + auto dl = Net::Download::makeCached(url, entry, options); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + out.append(dl); } else { - if(sha1.size()) - { - auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); - auto dl = Net::Download::makeCached(url, entry, options); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); - qDebug() << "Checksummed Download for:" << rawName() << "storage:" << storage << "url:" << url; - out.append(dl); - } - else - { - out.append(Net::Download::makeCached(url, entry, options)); - qDebug() << "Download for:" << rawName() << "storage:" << storage << "url:" << url; - } + out.append(Net::Download::makeCached(url, entry, options)); + qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; } return true; }; @@ -166,7 +145,7 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class } else { - qDebug() << "Ignoring native library" << m_name << "because it has no classifier for current OS"; + qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS"; } } else @@ -178,13 +157,14 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class } else { - qDebug() << "Ignoring java library" << m_name << "because it has no artifact"; + qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; } } } else { - auto raw_dl = [&](){ + auto raw_dl = [&]() + { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; @@ -192,7 +172,7 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class if (m_repositoryURL.isEmpty()) { - return QString("https://" + URLConstants::LIBRARY_BASE) + raw_storage; + return BuildConfig.LIBRARY_BASE + raw_storage; } if(m_repositoryURL.endsWith('/')) @@ -251,6 +231,11 @@ bool Library::isLocal() const return m_hint == "local"; } +bool Library::isAlwaysStale() const +{ + return m_hint == "always-stale"; +} + void Library::setStoragePrefix(QString prefix) { m_storagePrefix = prefix; diff --git a/api/logic/minecraft/Library.h b/api/logic/minecraft/Library.h index 5fcff316..acdd6c9c 100644 --- a/api/logic/minecraft/Library.h +++ b/api/logic/minecraft/Library.h @@ -12,7 +12,6 @@ #include "Rule.h" #include "minecraft/OpSys.h" #include "GradleSpecifier.h" -#include "net/URLConstants.h" #include "MojangDownloadInfo.h" #include "multimc_logic_export.h" @@ -148,9 +147,15 @@ public: /* methods */ /// Returns true if the library is contained in an instance and false if it is shared bool isLocal() const; + /// Returns true if the library is to always be checked for updates + bool isAlwaysStale() const; + + /// Return true if the library requires forge XZ hacks + bool isForge() const; + // Get a list of downloads for this library QList<NetActionPtr> getDownloads(OpSys system, class HttpMetaCache * cache, - QStringList & failedFiles, const QString & overridePath) const; + QStringList & failedLocalFiles, const QString & overridePath) const; private: /* methods */ /// the default storage prefix used by MultiMC diff --git a/api/logic/minecraft/Library_test.cpp b/api/logic/minecraft/Library_test.cpp index 4f9c8006..75bb4db1 100644 --- a/api/logic/minecraft/Library_test.cpp +++ b/api/logic/minecraft/Library_test.cpp @@ -18,7 +18,8 @@ private: jsonFile.open(QIODevice::ReadOnly); auto data = jsonFile.readAll(); jsonFile.close(); - return MojangVersionFormat::libraryFromJson(QJsonDocument::fromJson(data).object(), file); + ProblemContainer problems; + return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), file); } // get absolute path to expected storage, assuming default cache prefix QStringList getStorage(QString relative) @@ -65,7 +66,7 @@ slots: test.setHint("local"); auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 0); - QCOMPARE(failedFiles, getStorage("test/package/testname/testversion/testname-testversion.jar")); + QCOMPARE(failedFiles, {"testname-testversion.jar"}); } void test_legacy_url_local_override() { @@ -170,11 +171,11 @@ slots: QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, {QFileInfo("data/testname-testversion-linux-32.jar").absoluteFilePath()}); - QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")); + QCOMPARE(native64, {QFileInfo("data/testname-testversion-linux-64.jar").absoluteFilePath()}); QStringList failedFiles; auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); QCOMPARE(dls.size(), 0); - QCOMPARE(failedFiles, {getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")}); + QCOMPARE(failedFiles, {"data/testname-testversion-linux-64.jar"}); } } void test_onenine() diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 7c83b890..dbf9f816 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -1,5 +1,5 @@ #include "MinecraftInstance.h" -#include <minecraft/launch/CreateServerResourcePacksFolder.h> +#include <minecraft/launch/CreateGameFolders.h> #include <minecraft/launch/ExtractNatives.h> #include <minecraft/launch/PrintInstanceInfo.h> #include <settings/Setting.h> @@ -12,6 +12,7 @@ #include <java/JavaVersion.h> #include "launch/LaunchTask.h" +#include "launch/steps/LookupServerAddress.h" #include "launch/steps/PostLaunchCommand.h" #include "launch/steps/Update.h" #include "launch/steps/PreLaunchCommand.h" @@ -20,22 +21,28 @@ #include "minecraft/launch/DirectJavaLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" #include "minecraft/launch/ClaimAccount.h" +#include "minecraft/launch/ReconstructAssets.h" +#include "minecraft/launch/ScanModFolders.h" +#include "minecraft/launch/VerifyJavaInstall.h" #include "java/launch/CheckJava.h" #include "java/JavaUtils.h" #include "meta/Index.h" #include "meta/VersionList.h" -#include "SimpleModList.h" -#include "ModsModel.h" +#include "mod/ModFolderModel.h" +#include "mod/ResourcePackFolderModel.h" +#include "mod/TexturePackFolderModel.h" #include "WorldList.h" #include "icons/IIconList.h" #include <QCoreApplication> -#include "ComponentList.h" +#include "PackProfile.h" #include "AssetsUtils.h" #include "MinecraftUpdate.h" #include "MinecraftLoadAndCheck.h" +#include <minecraft/gameoptions/GameOptions.h> +#include <minecraft/update/FoldersTask.h> #define IBUS "@im=ibus" @@ -98,13 +105,27 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO 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); + + // Game time + auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); + m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); + m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // DEPRECATED: Read what versions the user configuration thinks should be used m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); m_settings->registerSetting("LWJGLVersion", ""); m_settings->registerSetting("ForgeVersion", ""); m_settings->registerSetting("LiteloaderVersion", ""); - m_components.reset(new ComponentList(this)); + m_components.reset(new PackProfile(this)); m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString()); auto setting = m_settings->getSetting("LWJGLVersion"); m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString()); @@ -122,14 +143,14 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } -std::shared_ptr<ComponentList> MinecraftInstance::getComponentList() const +std::shared_ptr<PackProfile> MinecraftInstance::getPackProfile() const { return m_components; } QSet<QString> MinecraftInstance::traits() const { - auto components = getComponentList(); + auto components = getPackProfile(); if (!components) { return {"version-incomplete"}; @@ -170,6 +191,12 @@ QString MinecraftInstance::getLocalLibraryPath() const return libraries_dir.absolutePath(); } +QString MinecraftInstance::jarModsDir() const +{ + QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); + return jarmods_dir.absolutePath(); +} + QString MinecraftInstance::loaderModsDir() const { return FS::PathCombine(gameRoot(), "mods"); @@ -200,11 +227,6 @@ QString MinecraftInstance::instanceConfigFolder() const return FS::PathCombine(gameRoot(), "config"); } -QString MinecraftInstance::jarModsDir() const -{ - return FS::PathCombine(instanceRoot(), "jarmods"); -} - QString MinecraftInstance::libDir() const { return FS::PathCombine(gameRoot(), "lib"); @@ -215,6 +237,11 @@ QString MinecraftInstance::worldDir() const return FS::PathCombine(gameRoot(), "saves"); } +QString MinecraftInstance::resourcesDir() const +{ + return FS::PathCombine(gameRoot(), "resources"); +} + QDir MinecraftInstance::librariesPath() const { return QDir::current().absoluteFilePath("libraries"); @@ -257,7 +284,7 @@ QStringList MinecraftInstance::getNativeJars() const QStringList MinecraftInstance::extraArguments() const { auto list = BaseInstance::extraArguments(); - auto version = getComponentList(); + auto version = getPackProfile(); if (!version) return list; auto jarMods = getJarMods(); @@ -376,7 +403,8 @@ static QString replaceTokensIn(QString text, QMap<QString, QString> with) return result; } -QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) const +QStringList MinecraftInstance::processMinecraftArgs( + AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const { auto profile = m_components->getProfile(); QString args_pattern = profile->getMinecraftArguments(); @@ -385,6 +413,12 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons args_pattern += " --tweakClass " + tweaker; } + if (serverToJoin && !serverToJoin->address.isEmpty()) + { + args_pattern += " --server " + serverToJoin->address; + args_pattern += " --port " + QString::number(serverToJoin->port); + } + QMap<QString, QString> token_mapping; // yggdrasil! if(session) @@ -407,8 +441,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons token_mapping["game_directory"] = absRootDir; QString absAssetsDir = QDir("assets/").absolutePath(); auto assets = profile->getMinecraftAssets(); - // FIXME: this is wrong and should be run as an async task - token_mapping["game_assets"] = AssetsUtils::reconstructAssets(assets->id).absolutePath(); + token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); // 1.7.3+ assets tokens token_mapping["assets_root"] = absAssetsDir; @@ -422,7 +455,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons return parts; } -QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QString launchScript; @@ -443,8 +476,17 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) launchScript += "appletClass " + appletClass + "\n"; } + if (serverToJoin && !serverToJoin->address.isEmpty()) + { + launchScript += "serverAddress " + serverToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n"; + } + // generic minecraft params - for (auto param : processMinecraftArgs(session)) + for (auto param : processMinecraftArgs( + session, + nullptr /* When using a launch script, the server parameters are handled by it*/ + )) { launchScript += "param " + param + "\n"; } @@ -494,7 +536,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) return launchScript; } -QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QStringList out; out << "Main Class:" << " " + getMainClass() << ""; @@ -513,11 +555,23 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) + { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << ""; + } + // libraries and class path. { out << "Libraries:"; QStringList jars, nativeJars; - auto javaArchitecture = settings()->get("JavaArchitecture").toString(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); auto printLibFile = [&](const QString & path) { @@ -544,37 +598,38 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } - if(loaderModList()->size()) - { - out << "Mods:"; - for(auto & mod: loaderModList()->allMods()) + auto printModList = [&](const QString & label, ModFolderModel & model) { + if(model.size()) { - if(!mod.enabled()) - continue; - if(mod.type() == Mod::MOD_FOLDER) - continue; - // TODO: proper implementation would need to descend into folders. + out << QString("%1:").arg(label); + auto modList = model.allMods(); + std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { + auto aName = a.filename().completeBaseName(); + auto bName = b.filename().completeBaseName(); + return aName.localeAwareCompare(bName) < 0; + }); + for(auto & mod: modList) + { + if(mod.type() == Mod::MOD_FOLDER) + { + out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)"; + continue; + } + + if(mod.enabled()) { + out << u8" [✔️] " + mod.filename().completeBaseName(); + } + else { + out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)"; + } - out << " " + mod.filename().completeBaseName(); + } + out << ""; } - out << ""; - } - - if(coreModList()->size()) - { - out << "Core Mods:"; - for(auto & coremod: coreModList()->allMods()) - { - if(!coremod.enabled()) - continue; - if(coremod.type() == Mod::MOD_FOLDER) - continue; - // TODO: proper implementation would need to descend into folders. + }; - out << " " + coremod.filename().completeBaseName(); - } - out << ""; - } + printModList("Mods", *(loaderModList().get())); + printModList("Core Mods", *(coreModList().get())); auto & jarMods = profile->getJarMods(); if(jarMods.size()) @@ -596,20 +651,20 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } - auto params = processMinecraftArgs(nullptr); + auto params = processMinecraftArgs(nullptr, serverToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) + if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; } else { - auto width = settings()->get("MinecraftWinWidth").toInt(); - auto height = settings()->get("MinecraftWinHeight").toInt(); + auto width = settings->get("MinecraftWinWidth").toInt(); + auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } out << ""; @@ -642,8 +697,7 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess auto i = sessionRef.u.properties.begin(); while (i != sessionRef.u.properties.end()) { - if(i.key() == "preferredLanguage") - { + if(i.value().length() <= 3) { ++i; continue; } @@ -744,9 +798,15 @@ QString MinecraftInstance::getStatusbarDescription() QString description; description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName())); - if(totalTimePlayed() > 0) + if(m_settings->get("ShowGameTime").toBool()) { - description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + if (lastTimePlayed() > 0) { + description.append(tr(", last played for %1").arg(prettifyTimeDuration(lastTimePlayed()))); + } + + if (totalTimePlayed() > 0) { + description.append(tr(", total played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + } } if(hasCrashed()) { @@ -765,28 +825,28 @@ shared_qobject_ptr<Task> MinecraftInstance::createUpdateTask(Net::Mode mode) } case Net::Mode::Online: { - return shared_qobject_ptr<Task>(new OneSixUpdate(this)); + return shared_qobject_ptr<Task>(new MinecraftUpdate(this)); } } return nullptr; } -std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session) +shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { - auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr())); + // FIXME: get rid of shared_from_this ... + auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this())); auto pptr = process.get(); ENV.icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { - process->appendStep(std::make_shared<TextPrint>(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::MultiMC)); + process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::MultiMC)); } // check java { - auto step = std::make_shared<CheckJava>(pptr); - process->appendStep(step); + process->appendStep(new CheckJava(pptr)); } // check launch method @@ -794,14 +854,34 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s QString method = launchMethod(); if(!validMethods.contains(method)) { - process->appendStep(std::make_shared<TextPrint>(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); + process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); return process; } + // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) + { + process->appendStep(new CreateGameFolders(pptr)); + } + + if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool()) + { + QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString(); + serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress))); + } + + if(serverToJoin && serverToJoin->port == 25565) + { + // Resolve server address to join on launch + auto *step = new LookupServerAddress(pptr); + step->setLookupAddress(serverToJoin->address); + step->setOutputAddressPtr(serverToJoin); + process->appendStep(step); + } + // run pre-launch command if that's needed if(getPreLaunchCommand().size()) { - auto step = std::make_shared<PreLaunchCommand>(pptr); + auto step = new PreLaunchCommand(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -809,36 +889,42 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s // if we aren't in offline mode,. if(session->status != AuthSession::PlayableOffline) { - process->appendStep(std::make_shared<ClaimAccount>(pptr, session)); - process->appendStep(std::make_shared<Update>(pptr, Net::Mode::Online)); + process->appendStep(new ClaimAccount(pptr, session)); + process->appendStep(new Update(pptr, Net::Mode::Online)); } else { - process->appendStep(std::make_shared<Update>(pptr, Net::Mode::Offline)); + process->appendStep(new Update(pptr, Net::Mode::Offline)); } // if there are any jar mods { - auto step = std::make_shared<ModMinecraftJar>(pptr); - process->appendStep(step); + process->appendStep(new ModMinecraftJar(pptr)); } - // print some instance info here... + // Scan mods folders for mods { - auto step = std::make_shared<PrintInstanceInfo>(pptr, session); - process->appendStep(step); + process->appendStep(new ScanModFolders(pptr)); } - // create the server-resource-packs folder (workaround for Minecraft bug MCL-3732) + // print some instance info here... { - auto step = std::make_shared<CreateServerResourcePacksFolder>(pptr); - process->appendStep(step); + process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); } // extract native jars if needed { - auto step = std::make_shared<ExtractNatives>(pptr); - process->appendStep(step); + process->appendStep(new ExtractNatives(pptr)); + } + + // reconstruct assets if needed + { + process->appendStep(new ReconstructAssets(pptr)); + } + + // verify that minimum Java requirements are met + { + process->appendStep(new VerifyJavaInstall(pptr)); } { @@ -846,16 +932,18 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s auto method = launchMethod(); if(method == "LauncherPart") { - auto step = std::make_shared<LauncherPartLaunch>(pptr); + auto step = new LauncherPartLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } else if (method == "DirectJava") { - auto step = std::make_shared<DirectJavaLaunch>(pptr); + auto step = new DirectJavaLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } } @@ -863,7 +951,7 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s // run post-exit command if that's needed if(getPostExitCommand().size()) { - auto step = std::make_shared<PostLaunchCommand>(pptr); + auto step = new PostLaunchCommand(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -886,53 +974,47 @@ JavaVersion MinecraftInstance::getJavaVersion() const return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr<SimpleModList> MinecraftInstance::loaderModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new SimpleModList(loaderModsDir())); + m_loader_mod_list.reset(new ModFolderModel(loaderModsDir())); + m_loader_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } - m_loader_mod_list->update(); return m_loader_mod_list; } -std::shared_ptr<ModsModel> MinecraftInstance::modsModel() const -{ - if (!m_mods_model) - { - m_mods_model.reset(new ModsModel(loaderModsDir(), coreModsDir(), modsCacheLocation())); - } - m_mods_model->update(); - return m_mods_model; -} - -std::shared_ptr<SimpleModList> MinecraftInstance::coreModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new SimpleModList(coreModsDir())); + m_core_mod_list.reset(new ModFolderModel(coreModsDir())); + m_core_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } - m_core_mod_list->update(); return m_core_mod_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::resourcePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new SimpleModList(resourcePacksDir())); + 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->update(); return m_resource_pack_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::texturePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new SimpleModList(texturePacksDir())); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); + m_texture_pack_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction); } - m_texture_pack_list->update(); return m_texture_pack_list; } @@ -945,6 +1027,15 @@ std::shared_ptr<WorldList> MinecraftInstance::worldList() const return m_world_list; } +std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const +{ + if (!m_game_options) + { + m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt"))); + } + return m_game_options; +} + QList< Mod > MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index 5f0fa353..05600797 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -1,16 +1,17 @@ #pragma once #include "BaseInstance.h" #include <java/JavaVersion.h> -#include "minecraft/Mod.h" +#include "minecraft/mod/Mod.h" #include <QProcess> #include <QDir> #include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" -class ModsModel; -class SimpleModList; +class ModFolderModel; class WorldList; +class GameOptions; class LaunchStep; -class ComponentList; +class PackProfile; class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance { @@ -44,6 +45,7 @@ public: QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; + QString resourcesDir() const; QDir jarmodsPath() const; QDir librariesPath() const; QDir versionsPath() const; @@ -63,24 +65,23 @@ public: ////// Profile management ////// - std::shared_ptr<ComponentList> getComponentList() const; + std::shared_ptr<PackProfile> getPackProfile() const; ////// Mod Lists ////// - std::shared_ptr<ModsModel> modsModel() const; - std::shared_ptr<SimpleModList> loaderModList() const; - std::shared_ptr<SimpleModList> coreModList() const; - std::shared_ptr<SimpleModList> resourcePackList() const; - std::shared_ptr<SimpleModList> texturePackList() const; + 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<WorldList> worldList() const; - + std::shared_ptr<GameOptions> gameOptionsModel() const; ////// Launch stuff ////// shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override; - std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override; + shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; QStringList extraArguments() const override; - QStringList verboseDescription(AuthSessionPtr session) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; QList<Mod> getJarMods() const; - QString createLaunchScript(AuthSessionPtr session); + QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); /// get arguments passed to java QStringList javaArguments() const; @@ -107,13 +108,10 @@ public: virtual QString getMainClass() const; // FIXME: remove - virtual QStringList processMinecraftArgs(AuthSessionPtr account) const; + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const; virtual JavaVersion getJavaVersion() const; -signals: - void versionReloaded(); - protected: QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session); QStringList validLaunchMethods(); @@ -123,13 +121,13 @@ private: QString prettifyTimeDuration(int64_t duration); protected: // data - std::shared_ptr<ComponentList> m_components; - mutable std::shared_ptr<ModsModel> m_mods_model; - mutable std::shared_ptr<SimpleModList> m_loader_mod_list; - mutable std::shared_ptr<SimpleModList> m_core_mod_list; - mutable std::shared_ptr<SimpleModList> m_resource_pack_list; - mutable std::shared_ptr<SimpleModList> m_texture_pack_list; + 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_texture_pack_list; mutable std::shared_ptr<WorldList> m_world_list; + mutable std::shared_ptr<GameOptions> m_game_options; }; typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr; diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.cpp b/api/logic/minecraft/MinecraftLoadAndCheck.cpp index a5052b53..79b0c484 100644 --- a/api/logic/minecraft/MinecraftLoadAndCheck.cpp +++ b/api/logic/minecraft/MinecraftLoadAndCheck.cpp @@ -1,6 +1,6 @@ #include "MinecraftLoadAndCheck.h" #include "MinecraftInstance.h" -#include "ComponentList.h" +#include "PackProfile.h" MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { @@ -9,7 +9,7 @@ MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *p void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); components->reload(Net::Mode::Offline); m_task = components->getCurrentTask(); @@ -28,7 +28,7 @@ void MinecraftLoadAndCheck::subtaskSucceeded() { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; return; } emitSucceeded(); @@ -38,7 +38,7 @@ void MinecraftLoadAndCheck::subtaskFailed(QString error) { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "failed, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; return; } emitFailed(error); diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.h b/api/logic/minecraft/MinecraftLoadAndCheck.h index 1f5c2018..3435b52b 100644 --- a/api/logic/minecraft/MinecraftLoadAndCheck.h +++ b/api/logic/minecraft/MinecraftLoadAndCheck.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/MinecraftUpdate.cpp b/api/logic/minecraft/MinecraftUpdate.cpp index e62ff745..8f1565b0 100644 --- a/api/logic/minecraft/MinecraftUpdate.cpp +++ b/api/logic/minecraft/MinecraftUpdate.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -14,7 +14,6 @@ */ #include "Env.h" -#include <minecraft/forge/ForgeXzDownload.h> #include "MinecraftUpdate.h" #include "MinecraftInstance.h" @@ -24,9 +23,8 @@ #include <QDataStream> #include "BaseInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "minecraft/Library.h" -#include "net/URLConstants.h" #include <FileSystem.h> #include "update/FoldersTask.h" @@ -37,11 +35,11 @@ #include <meta/Index.h> #include <meta/Version.h> -OneSixUpdate::OneSixUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +MinecraftUpdate::MinecraftUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { } -void OneSixUpdate::executeTask() +void MinecraftUpdate::executeTask() { m_tasks.clear(); // create folders @@ -51,7 +49,7 @@ void OneSixUpdate::executeTask() // add metadata update task if necessary { - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); components->reload(Net::Mode::Online); auto task = components->getCurrentTask(); if(task) @@ -83,7 +81,7 @@ void OneSixUpdate::executeTask() next(); } -void OneSixUpdate::next() +void MinecraftUpdate::next() { if(m_abort) { @@ -99,10 +97,10 @@ void OneSixUpdate::next() if(m_currentTask > 0) { auto task = m_tasks[m_currentTask - 1]; - disconnect(task.get(), &Task::succeeded, this, &OneSixUpdate::subtaskSucceeded); - disconnect(task.get(), &Task::failed, this, &OneSixUpdate::subtaskFailed); - disconnect(task.get(), &Task::progress, this, &OneSixUpdate::progress); - disconnect(task.get(), &Task::status, this, &OneSixUpdate::setStatus); + disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); + disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); } if(m_currentTask == m_tasks.size()) { @@ -113,13 +111,13 @@ void OneSixUpdate::next() // if the task is already finished by the time we look at it, skip it if(task->isFinished()) { - qCritical() << "OneSixUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); + qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); next(); } - connect(task.get(), &Task::succeeded, this, &OneSixUpdate::subtaskSucceeded); - connect(task.get(), &Task::failed, this, &OneSixUpdate::subtaskFailed); - connect(task.get(), &Task::progress, this, &OneSixUpdate::progress); - connect(task.get(), &Task::status, this, &OneSixUpdate::setStatus); + connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); + connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); // if the task is already running, do not start it again if(!task->isRunning()) { @@ -127,35 +125,35 @@ void OneSixUpdate::next() } } -void OneSixUpdate::subtaskSucceeded() +void MinecraftUpdate::subtaskSucceeded() { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; return; } auto senderTask = QObject::sender(); auto currentTask = m_tasks[m_currentTask].get(); if(senderTask != currentTask) { - qDebug() << "OneSixUpdate: Subtask" << sender() << "succeeded out of order."; + qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order."; return; } next(); } -void OneSixUpdate::subtaskFailed(QString error) +void MinecraftUpdate::subtaskFailed(QString error) { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "failed, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; return; } auto senderTask = QObject::sender(); auto currentTask = m_tasks[m_currentTask].get(); if(senderTask != currentTask) { - qDebug() << "OneSixUpdate: Subtask" << sender() << "failed out of order."; + qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order."; m_failed_out_of_order = true; m_fail_reason = error; return; @@ -164,7 +162,7 @@ void OneSixUpdate::subtaskFailed(QString error) } -bool OneSixUpdate::abort() +bool MinecraftUpdate::abort() { if(!m_abort) { @@ -178,7 +176,7 @@ bool OneSixUpdate::abort() return true; } -bool OneSixUpdate::canAbort() const +bool MinecraftUpdate::canAbort() const { return true; } diff --git a/api/logic/minecraft/MinecraftUpdate.h b/api/logic/minecraft/MinecraftUpdate.h index f7b37d73..fadebff9 100644 --- a/api/logic/minecraft/MinecraftUpdate.h +++ b/api/logic/minecraft/MinecraftUpdate.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -27,12 +27,12 @@ class MinecraftVersion; class MinecraftInstance; -class OneSixUpdate : public Task +class MinecraftUpdate : public Task { Q_OBJECT public: - explicit OneSixUpdate(MinecraftInstance *inst, QObject *parent = 0); - virtual ~OneSixUpdate() {}; + explicit MinecraftUpdate(MinecraftInstance *inst, QObject *parent = 0); + virtual ~MinecraftUpdate() {}; void executeTask() override; bool canAbort() const override; diff --git a/api/logic/minecraft/Mod.cpp b/api/logic/minecraft/Mod.cpp deleted file mode 100644 index bd209211..00000000 --- a/api/logic/minecraft/Mod.cpp +++ /dev/null @@ -1,378 +0,0 @@ -/* Copyright 2013-2018 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 <QDir> -#include <QString> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> -#include <QJsonValue> -#include <quazip.h> -#include <quazipfile.h> - -#include "Mod.h" -#include "settings/INIFile.h" -#include <FileSystem.h> -#include <QDebug> - -Mod::Mod(const QFileInfo &file) -{ - repath(file); - m_changedDateTime = file.lastModified(); -} - -void Mod::repath(const QFileInfo &file) -{ - m_file = file; - QString name_base = file.fileName(); - - m_type = Mod::MOD_UNKNOWN; - - if (m_file.isDir()) - { - m_type = MOD_FOLDER; - m_name = name_base; - m_mmc_id = name_base; - } - else if (m_file.isFile()) - { - if (name_base.endsWith(".disabled")) - { - m_enabled = false; - name_base.chop(9); - } - else - { - m_enabled = true; - } - m_mmc_id = name_base; - 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; - } - - if (m_type == MOD_ZIPFILE) - { - QuaZip zip(m_file.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("mcmod.info")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - ReadMCModInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - else if (zip.setCurrentFile("forgeversion.properties")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - ReadForgeInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - - zip.close(); - } - else if (m_type == MOD_FOLDER) - { - QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info")); - if (mcmod_info.isFile()) - { - QFile mcmod(mcmod_info.filePath()); - if (!mcmod.open(QIODevice::ReadOnly)) - return; - auto data = mcmod.readAll(); - if (data.isEmpty() || data.isNull()) - return; - ReadMCModInfo(data); - } - } - else if (m_type == MOD_LITEMOD) - { - QuaZip zip(m_file.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("litemod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - ReadLiteModInfo(file.readAll()); - file.close(); - } - zip.close(); - } -} - -// NEW format -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 - -// OLD format: -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc -void Mod::ReadMCModInfo(QByteArray contents) -{ - auto getInfoFromArray = [&](QJsonArray arr)->void - { - if (!arr.at(0).isObject()) - return; - auto firstObj = arr.at(0).toObject(); - m_mod_id = firstObj.value("modid").toString(); - m_name = firstObj.value("name").toString(); - m_version = firstObj.value("version").toString(); - m_homeurl = firstObj.value("url").toString(); - m_updateurl = firstObj.value("updateUrl").toString(); - m_homeurl = m_homeurl.trimmed(); - if(!m_homeurl.isEmpty()) - { - // fix up url. - if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") && - !m_homeurl.startsWith("ftp://")) - { - m_homeurl.prepend("http://"); - } - } - m_description = firstObj.value("description").toString(); - QJsonArray authors = firstObj.value("authorList").toArray(); - if (authors.size() == 0) - authors = firstObj.value("authors").toArray(); - - if (authors.size() == 0) - m_authors = ""; - else if (authors.size() >= 1) - { - m_authors = authors.at(0).toString(); - for (int i = 1; i < authors.size(); i++) - { - m_authors += ", " + authors.at(i).toString(); - } - } - m_credits = firstObj.value("credits").toString(); - return; - } - ; - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - // this is the very old format that had just the array - if (jsonDoc.isArray()) - { - getInfoFromArray(jsonDoc.array()); - } - else if (jsonDoc.isObject()) - { - auto val = jsonDoc.object().value("modinfoversion"); - if(val.isUndefined()) - val = jsonDoc.object().value("modListVersion"); - int version = val.toDouble(); - if (version != 2) - { - qCritical() << "BAD stuff happened to mod json:"; - qCritical() << contents; - return; - } - auto arrVal = jsonDoc.object().value("modlist"); - if(arrVal.isUndefined()) - arrVal = jsonDoc.object().value("modList"); - if (arrVal.isArray()) - { - getInfoFromArray(arrVal.toArray()); - } - } -} - -void Mod::ReadForgeInfo(QByteArray contents) -{ - // Read the data - m_name = "Minecraft Forge"; - m_mod_id = "Forge"; - m_homeurl = "http://www.minecraftforge.net/forum/"; - INIFile ini; - if (!ini.loadFile(contents)) - return; - - QString major = ini.get("forge.major.number", "0").toString(); - QString minor = ini.get("forge.minor.number", "0").toString(); - QString revision = ini.get("forge.revision.number", "0").toString(); - QString build = ini.get("forge.build.number", "0").toString(); - - m_version = major + "." + minor + "." + revision + "." + build; -} - -void Mod::ReadLiteModInfo(QByteArray contents) -{ - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - auto object = jsonDoc.object(); - if (object.contains("name")) - { - m_mod_id = m_name = object.value("name").toString(); - } - if (object.contains("version")) - { - m_version = object.value("version").toString(""); - } - else - { - m_version = object.value("revision").toString(""); - } - m_mcversion = object.value("mcversion").toString(); - m_authors = object.value("author").toString(); - m_description = object.value("description").toString(); - m_homeurl = object.value("url").toString(); -} - -bool Mod::replace(Mod &with) -{ - if (!destroy()) - return false; - bool success = false; - auto t = with.type(); - - if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD) - { - qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath(); - success = QFile::copy(with.m_file.filePath(), m_file.filePath()); - } - if (t == MOD_FOLDER) - { - success = FS::copy(with.m_file.filePath(), m_file.path())(); - } - if (success) - { - m_name = with.m_name; - m_mmc_id = with.m_mmc_id; - m_mod_id = with.m_mod_id; - m_version = with.m_version; - m_mcversion = with.m_mcversion; - m_description = with.m_description; - m_authors = with.m_authors; - m_credits = with.m_credits; - m_homeurl = with.m_homeurl; - m_type = with.m_type; - m_file.refresh(); - } - return success; -} - -bool Mod::destroy() -{ - if (m_type == MOD_FOLDER) - { - QDir d(m_file.filePath()); - if (d.removeRecursively()) - { - m_type = MOD_UNKNOWN; - return true; - } - return false; - } - else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) - { - QFile f(m_file.filePath()); - if (f.remove()) - { - m_type = MOD_UNKNOWN; - return true; - } - return false; - } - return true; -} - -QString Mod::version() const -{ - switch (type()) - { - case MOD_ZIPFILE: - case MOD_LITEMOD: - return m_version; - case MOD_FOLDER: - return "Folder"; - case MOD_SINGLEFILE: - return "File"; - default: - return "VOID"; - } -} - -bool Mod::enable(bool value) -{ - if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) - return false; - - if (m_enabled == value) - return false; - - QString path = m_file.absoluteFilePath(); - if (value) - { - QFile foo(path); - if (!path.endsWith(".disabled")) - return false; - path.chop(9); - if (!foo.rename(path)) - return false; - } - else - { - QFile foo(path); - path += ".disabled"; - if (!foo.rename(path)) - return false; - } - m_file = QFileInfo(path); - m_enabled = value; - return true; -} -bool Mod::operator==(const Mod &other) const -{ - return mmc_id() == other.mmc_id(); -} -bool Mod::strongCompare(const Mod &other) const -{ - return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type(); -} diff --git a/api/logic/minecraft/ModsModel.cpp b/api/logic/minecraft/ModsModel.cpp deleted file mode 100644 index e401618a..00000000 --- a/api/logic/minecraft/ModsModel.cpp +++ /dev/null @@ -1,374 +0,0 @@ -/* Copyright 2013-2018 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 "ModsModel.h" -#include <FileSystem.h> -#include <QMimeData> -#include <QUrl> -#include <QUuid> -#include <QString> -#include <QFileSystemWatcher> -#include <QDebug> - -ModsModel::ModsModel(const QString &mainDir, const QString &coreDir, const QString &cacheLocation) - :QAbstractListModel(), m_mainDir(mainDir), m_coreDir(coreDir) -{ - FS::ensureFolderPathExists(m_mainDir.absolutePath()); - m_mainDir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); - m_mainDir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); - m_watcher = new QFileSystemWatcher(this); - connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); -} - -void ModsModel::startWatching() -{ - if(is_watching) - return; - - update(); - - is_watching = m_watcher->addPath(m_mainDir.absolutePath()); - if (is_watching) - { - qDebug() << "Started watching " << m_mainDir.absolutePath(); - } - else - { - qDebug() << "Failed to start watching " << m_mainDir.absolutePath(); - } -} - -void ModsModel::stopWatching() -{ - if(!is_watching) - return; - - is_watching = !m_watcher->removePath(m_mainDir.absolutePath()); - if (!is_watching) - { - qDebug() << "Stopped watching " << m_mainDir.absolutePath(); - } - else - { - qDebug() << "Failed to stop watching " << m_mainDir.absolutePath(); - } -} - -bool ModsModel::update() -{ - if (!isValid()) - return false; - - QList<Mod> orderedMods; - QList<Mod> newMods; - m_mainDir.refresh(); - auto folderContents = m_mainDir.entryInfoList(); - bool orderOrStateChanged = false; - - // if there are any untracked files... - if (folderContents.size()) - { - // the order surely changed! - for (auto entry : folderContents) - { - newMods.append(Mod(entry)); - } - orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } - } - beginResetModel(); - mods.swap(orderedMods); - endResetModel(); - if (orderOrStateChanged) - { - emit changed(); - } - return true; -} - -void ModsModel::directoryChanged(QString path) -{ - update(); -} - -bool ModsModel::isValid() -{ - return m_mainDir.exists() && m_mainDir.isReadable(); -} - -bool ModsModel::installMod(const QString &filename) -{ - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - QFileInfo fileinfo(FS::NormalizePath(filename)); - - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - return false; - } - Mod m(fileinfo); - if (!m.valid()) - return false; - - auto type = m.type(); - if (type == Mod::MOD_UNKNOWN) - return false; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - QString newpath = FS::PathCombine(m_mainDir.path(), fileinfo.fileName()); - if (!QFile::copy(fileinfo.filePath(), newpath)) - return false; - FS::updateTimestamp(newpath); - m.repath(newpath); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - QString to = FS::PathCombine(m_mainDir.path(), fileinfo.fileName()); - if (!FS::copy(from, to)()) - return false; - m.repath(to); - update(); - return true; - } - return false; -} - -bool ModsModel::enableMods(const QModelIndexList& indexes, bool enable) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.enable(enable); - emit dataChanged(i, i); - } - emit changed(); - return true; -} - -bool ModsModel::deleteMods(const QModelIndexList& indexes) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.destroy(); - } - emit changed(); - return true; -} - -int ModsModel::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -QVariant ModsModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - 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(); - case VersionColumn: - return mods[row].version(); - case DateColumn: - return mods[row].dateTimeChanged(); - case LocationColumn: - return "Unknown"; - - default: - return QVariant(); - } - - case Qt::ToolTipRole: - return mods[row].mmc_id(); - - case Qt::CheckStateRole: - switch (column) - { - case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } - default: - return QVariant(); - } -} - -bool ModsModel::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) - { - auto &mod = mods[index.row()]; - if (mod.enable(!mod.enabled())) - { - emit dataChanged(index, index); - return true; - } - } - return false; -} - -QVariant ModsModel::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 VersionColumn: - return tr("Version"); - case DateColumn: - return tr("Last changed"); - case LocationColumn: - return tr("Location"); - default: - return QVariant(); - } - - case Qt::ToolTipRole: - switch (section) - { - case ActiveColumn: - return tr("Is the mod enabled?"); - case NameColumn: - return tr("The name of the mod."); - case VersionColumn: - return tr("The version of the mod."); - case DateColumn: - return tr("The date and time this mod was last changed (or added)."); - case LocationColumn: - return tr("Where the mod is located (inside or outside the instance)."); - default: - return QVariant(); - } - default: - return QVariant(); - } - return QVariant(); -} - -Qt::ItemFlags ModsModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | - defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; -} - -Qt::DropActions ModsModel::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList ModsModel::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool ModsModel::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()) - { - bool was_watching = is_watching; - if (was_watching) - { - stopWatching(); - } - 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 (was_watching) - { - startWatching(); - } - return true; - } - return false; -} diff --git a/api/logic/minecraft/ModsModel.h b/api/logic/minecraft/ModsModel.h deleted file mode 100644 index b8980ac4..00000000 --- a/api/logic/minecraft/ModsModel.h +++ /dev/null @@ -1,123 +0,0 @@ -/* Copyright 2013-2018 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <QList> -#include <QString> -#include <QDir> -#include <QAbstractListModel> - -#include "minecraft/Mod.h" - -#include "multimc_logic_export.h" - -class LegacyInstance; -class BaseInstance; -class QFileSystemWatcher; - -/** - * A legacy mod list. - * Backed by a folder. - */ -class MULTIMC_LOGIC_EXPORT ModsModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum Columns - { - ActiveColumn = 0, - NameColumn, - DateColumn, - VersionColumn, - LocationColumn, - NUM_COLUMNS - }; - ModsModel(const QString &mainDir, const QString &coreDir, const QString &cacheFile); - - 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; - - /// 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; - - 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]; - } - - /// Reloads the mod list and returns true if the list changed. - virtual bool update(); - - /** - * Adds the given mod to the list at the given index - if the list supports custom ordering - */ - bool installMod(const QString& filename); - - /// Deletes all the selected mods - virtual bool deleteMods(const QModelIndexList &indexes); - - /// Enable or disable listed mods - virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); - - void startWatching(); - void stopWatching(); - - virtual bool isValid(); - - QDir dir() - { - return m_mainDir; - } - - const QList<Mod> & allMods() - { - return mods; - } - -private -slots: - void directoryChanged(QString path); - -signals: - void changed(); - -protected: - QFileSystemWatcher *m_watcher; - bool is_watching = false; - QDir m_mainDir; - QDir m_coreDir; - QList<Mod> mods; -}; diff --git a/api/logic/minecraft/MojangVersionFormat.cpp b/api/logic/minecraft/MojangVersionFormat.cpp index 33d3c54c..f9cb2228 100644 --- a/api/logic/minecraft/MojangVersionFormat.cpp +++ b/api/logic/minecraft/MojangVersionFormat.cpp @@ -220,7 +220,7 @@ VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc { auto libObj = requireObject(libVal); - auto lib = MojangVersionFormat::libraryFromJson(libObj, filename); + auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); out->libraries.append(lib); } } @@ -283,14 +283,18 @@ QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch } } -LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); } - out->m_name = libObj.value("name").toString(); + auto rawName = libObj.value("name").toString(); + out->m_name = rawName; + if(!out->m_name.valid()) { + problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); + } Bits::readString(libObj, "url", out->m_repositoryURL); if (libObj.contains("extract")) @@ -333,7 +337,7 @@ LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QJsonObject MojangVersionFormat::libraryToJson(Library *library) { QJsonObject libRoot; - libRoot.insert("name", (QString)library->m_name); + libRoot.insert("name", library->m_name.serialize()); if (!library->m_repositoryURL.isEmpty()) { libRoot.insert("url", library->m_repositoryURL); diff --git a/api/logic/minecraft/MojangVersionFormat.h b/api/logic/minecraft/MojangVersionFormat.h index 76c529e9..2871dae4 100644 --- a/api/logic/minecraft/MojangVersionFormat.h +++ b/api/logic/minecraft/MojangVersionFormat.h @@ -3,6 +3,7 @@ #include <minecraft/VersionFile.h> #include <minecraft/Library.h> #include <QJsonDocument> +#include <ProblemProvider.h> #include "multimc_logic_export.h" @@ -20,6 +21,6 @@ public: static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries - static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject libraryToJson(Library *library); }; diff --git a/api/logic/minecraft/OneSixVersionFormat.cpp b/api/logic/minecraft/OneSixVersionFormat.cpp index 6f3b926b..d6aaa790 100644 --- a/api/logic/minecraft/OneSixVersionFormat.cpp +++ b/api/logic/minecraft/OneSixVersionFormat.cpp @@ -13,9 +13,9 @@ static void readString(const QJsonObject &root, const QString &key, QString &var } } -LibraryPtr OneSixVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) { - LibraryPtr out = MojangVersionFormat::libraryFromJson(libObj, filename); + LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); readString(libObj, "MMC-hint", out->m_hint); readString(libObj, "MMC-absulute_url", out->m_absoluteURL); readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); @@ -115,7 +115,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::jarModFromJson(libObj, filename); + auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); // and add to jar mods out->jarMods.append(lib); } @@ -126,7 +126,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::plusJarModFromJson(libObj, filename, out->name); + auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); // and add to jar mods out->jarMods.append(lib); } @@ -138,20 +138,20 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::modFromJson(libObj, filename); + auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); // and add to jar mods out->mods.append(lib); } } - auto readLibs = [&](const char * which) + auto readLibs = [&](const char * which, QList<LibraryPtr> & outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library - auto lib = libraryFromJson(libObj, filename); - out->libraries.append(lib); + auto lib = libraryFromJson(*out, libObj, filename); + outList.append(lib); } }; bool hasPlusLibs = root.contains("+libraries"); @@ -160,23 +160,27 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { out->addProblem(ProblemSeverity::Warning, QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); - readLibs("libraries"); - readLibs("+libraries"); + readLibs("libraries", out->libraries); + readLibs("+libraries", out->libraries); } else if (hasLibs) { - readLibs("libraries"); + readLibs("libraries", out->libraries); } else if(hasPlusLibs) { - readLibs("+libraries"); + readLibs("+libraries", out->libraries); + } + + if(root.contains("mavenFiles")) { + readLibs("mavenFiles", out->mavenFiles); } // if we have mainJar, just use it if(root.contains("mainJar")) { QJsonObject libObj = requireObject(root, "mainJar"); - out->mainJar = libraryFromJson(libObj, filename); + out->mainJar = libraryFromJson(*out, libObj, filename); } // else reconstruct it from downloads and id ... if that's available else if(!out->minecraftVersion.isEmpty()) @@ -194,7 +198,10 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc // FIXME: this will eventually break... else { - lib->setAbsoluteUrl(URLConstants::getLegacyJarUrl(out->minecraftVersion)); + out->addProblem( + ProblemSeverity::Error, + QObject::tr("URL for the main jar could not be determined - Mojang removed the server that we used as fallback.") + ); } out->mainJar = lib; } @@ -276,6 +283,15 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } root.insert("libraries", array); } + if (!patch->mavenFiles.isEmpty()) + { + QJsonArray array; + for (auto value: patch->mavenFiles) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("mavenFiles", array); + } if (!patch->jarMods.isEmpty()) { QJsonArray array; @@ -314,8 +330,12 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } } -LibraryPtr OneSixVersionFormat::plusJarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName) -{ +LibraryPtr OneSixVersionFormat::plusJarModFromJson( + ProblemContainer & problems, + const QJsonObject &libObj, + const QString &filename, + const QString &originalName +) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { @@ -350,9 +370,9 @@ LibraryPtr OneSixVersionFormat::plusJarModFromJson(const QJsonObject &libObj, co return out; } -LibraryPtr OneSixVersionFormat::jarModFromJson(const QJsonObject& libObj, const QString& filename) +LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) { - return libraryFromJson(libObj, filename); + return libraryFromJson(problems, libObj, filename); } @@ -361,9 +381,9 @@ QJsonObject OneSixVersionFormat::jarModtoJson(Library *jarmod) return libraryToJson(jarmod); } -LibraryPtr OneSixVersionFormat::modFromJson(const QJsonObject& libObj, const QString& filename) +LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) { - return libraryFromJson(libObj, filename); + return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::modtoJson(Library *jarmod) diff --git a/api/logic/minecraft/OneSixVersionFormat.h b/api/logic/minecraft/OneSixVersionFormat.h index 6bbebca0..1a091d88 100644 --- a/api/logic/minecraft/OneSixVersionFormat.h +++ b/api/logic/minecraft/OneSixVersionFormat.h @@ -1,9 +1,10 @@ #pragma once #include <minecraft/VersionFile.h> -#include <minecraft/ComponentList.h> +#include <minecraft/PackProfile.h> #include <minecraft/Library.h> #include <QJsonDocument> +#include <ProblemProvider.h> class OneSixVersionFormat { @@ -13,17 +14,17 @@ public: static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries - static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject libraryToJson(Library *library); // DEPRECATED: old 'plus' jar mods generated by the application - static LibraryPtr plusJarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName); + static LibraryPtr plusJarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename, const QString &originalName); // new jar mods derived from libraries - static LibraryPtr jarModFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr jarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject jarModtoJson(Library * jarmod); // mods, also derived from libraries - static LibraryPtr modFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr modFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject modtoJson(Library * jarmod); }; diff --git a/api/logic/minecraft/OpSys.cpp b/api/logic/minecraft/OpSys.cpp index 2e18634b..f6a4ed1c 100644 --- a/api/logic/minecraft/OpSys.cpp +++ b/api/logic/minecraft/OpSys.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/OpSys.h b/api/logic/minecraft/OpSys.h index 8ea84587..63c750b1 100644 --- a/api/logic/minecraft/OpSys.h +++ b/api/logic/minecraft/OpSys.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/ComponentList.cpp b/api/logic/minecraft/PackProfile.cpp index 7a865c60..f6918116 100644 --- a/api/logic/minecraft/ComponentList.cpp +++ b/api/logic/minecraft/PackProfile.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -32,21 +32,23 @@ #include <QTimer> #include <Json.h> -#include "ComponentList.h" -#include "ComponentList_p.h" +#include "PackProfile.h" +#include "PackProfile_p.h" #include "ComponentUpdateTask.h" -ComponentList::ComponentList(MinecraftInstance * instance) +PackProfile::PackProfile(MinecraftInstance * instance) : QAbstractListModel() { - d.reset(new ComponentListData); + d.reset(new PackProfileData); d->m_instance = instance; d->m_saveTimer.setSingleShot(true); d->m_saveTimer.setInterval(5000); - connect(&d->m_saveTimer, &QTimer::timeout, this, &ComponentList::save_internal); + d->interactionDisabled = instance->isRunning(); + connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); + connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); } -ComponentList::~ComponentList() +PackProfile::~PackProfile() { saveNow(); } @@ -95,7 +97,7 @@ static QJsonObject componentToJsonV1(ComponentPtr component) return obj; } -static ComponentPtr componentFromJsonV1(ComponentList * parent, const QString & componentJsonPattern, const QJsonObject &obj) +static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & componentJsonPattern, const QJsonObject &obj) { // critical auto uid = Json::requireString(obj.value("uid")); @@ -118,7 +120,7 @@ static ComponentPtr componentFromJsonV1(ComponentList * parent, const QString & } // Save the given component container data to a file -static bool saveComponentList(const QString & filename, const ComponentContainer & container) +static bool savePackProfile(const QString & filename, const ComponentContainer & container) { QJsonObject obj; obj.insert("formatVersion", currentComponentsFileVersion); @@ -151,7 +153,7 @@ static bool saveComponentList(const QString & filename, const ComponentContainer } // Read the given file into component containers -static bool loadComponentList(ComponentList * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container) +static bool loadPackProfile(PackProfile * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container) { QFile componentsFile(filename); if (!componentsFile.exists()) @@ -208,7 +210,7 @@ static bool loadComponentList(ComponentList * parent, const QString & filename, // BEGIN: save/load logic -void ComponentList::saveNow() +void PackProfile::saveNow() { if(saveIsScheduled()) { @@ -217,18 +219,18 @@ void ComponentList::saveNow() } } -bool ComponentList::saveIsScheduled() const +bool PackProfile::saveIsScheduled() const { return d->dirty; } -void ComponentList::buildingFromScratch() +void PackProfile::buildingFromScratch() { d->loaded = true; d->dirty = true; } -void ComponentList::scheduleSave() +void PackProfile::scheduleSave() { if(!d->loaded) { @@ -243,30 +245,30 @@ void ComponentList::scheduleSave() d->m_saveTimer.start(); } -QString ComponentList::componentsFilePath() const +QString PackProfile::componentsFilePath() const { return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); } -QString ComponentList::patchesPattern() const +QString PackProfile::patchesPattern() const { return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); } -QString ComponentList::patchFilePathForUid(const QString& uid) const +QString PackProfile::patchFilePathForUid(const QString& uid) const { return patchesPattern().arg(uid); } -void ComponentList::save_internal() +void PackProfile::save_internal() { qDebug() << "Component list save performed now for" << d->m_instance->name(); auto filename = componentsFilePath(); - saveComponentList(filename, d->components); + savePackProfile(filename, d->components); d->dirty = false; } -bool ComponentList::load() +bool PackProfile::load() { auto filename = componentsFilePath(); QFile componentsFile(filename); @@ -284,7 +286,7 @@ bool ComponentList::load() // load the new component list and swap it with the current one... ComponentContainer newComponents; - if(!loadComponentList(this, filename, patchesPattern(), newComponents)) + if(!loadPackProfile(this, filename, patchesPattern(), newComponents)) { qCritical() << "Failed to load the component config for instance" << d->m_instance->name(); return false; @@ -296,7 +298,7 @@ bool ComponentList::load() // disconnect all the old components for(auto component: d->components) { - disconnect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); } d->components.clear(); d->componentIndex.clear(); @@ -307,7 +309,7 @@ bool ComponentList::load() qWarning() << "Ignoring duplicate component entry" << component->m_uid; continue; } - connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); d->components.append(component); d->componentIndex[component->m_uid] = component; } @@ -317,7 +319,7 @@ bool ComponentList::load() } } -void ComponentList::reload(Net::Mode netmode) +void PackProfile::reload(Net::Mode netmode) { // Do not reload when the update/resolve task is running. It is in control. if(d->m_updateTask) @@ -337,29 +339,29 @@ void ComponentList::reload(Net::Mode netmode) } } -shared_qobject_ptr<Task> ComponentList::getCurrentTask() +shared_qobject_ptr<Task> PackProfile::getCurrentTask() { return d->m_updateTask; } -void ComponentList::resolve(Net::Mode netmode) +void PackProfile::resolve(Net::Mode netmode) { auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); d->m_updateTask.reset(updateTask); - connect(updateTask, &ComponentUpdateTask::succeeded, this, &ComponentList::updateSucceeded); - connect(updateTask, &ComponentUpdateTask::failed, this, &ComponentList::updateFailed); + connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); + connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); d->m_updateTask->start(); } -void ComponentList::updateSucceeded() +void PackProfile::updateSucceeded() { qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name(); d->m_updateTask.reset(); invalidateLaunchProfile(); } -void ComponentList::updateFailed(const QString& error) +void PackProfile::updateFailed(const QString& error) { qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error; d->m_updateTask.reset(); @@ -429,7 +431,7 @@ static void upgradeDeprecatedFiles(QString root, QString instanceName) * - Part is taken from the old order.json file. * - Part is loaded from loose json files in the instance's `patches` directory. */ -bool ComponentList::migratePreComponentConfig() +bool PackProfile::migratePreComponentConfig() { // upgrade the very old files from the beginnings of MultiMC 5 upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name()); @@ -596,17 +598,17 @@ bool ComponentList::migratePreComponentConfig() } } // new we have a complete list of components... - return saveComponentList(componentsFilePath(), components); + return savePackProfile(componentsFilePath(), components); } // END: save/load -void ComponentList::appendComponent(ComponentPtr component) +void PackProfile::appendComponent(ComponentPtr component) { insertComponent(d->components.size(), component); } -void ComponentList::insertComponent(size_t index, ComponentPtr component) +void PackProfile::insertComponent(size_t index, ComponentPtr component) { auto id = component->getID(); if(id.isEmpty()) @@ -623,18 +625,21 @@ void ComponentList::insertComponent(size_t index, ComponentPtr component) d->components.insert(index, component); d->componentIndex[id] = component; endInsertRows(); - connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); scheduleSave(); } -void ComponentList::componentDataChanged() +void PackProfile::componentDataChanged() { auto objPtr = qobject_cast<Component *>(sender()); if(!objPtr) { - qWarning() << "ComponentList got dataChenged signal from a non-Component!"; + qWarning() << "PackProfile got dataChenged signal from a non-Component!"; return; } + if(objPtr->getID() == "net.minecraft") { + emit minecraftChanged(); + } // figure out which one is it... in a seriously dumb way. int index = 0; for (auto component: d->components) @@ -647,10 +652,10 @@ void ComponentList::componentDataChanged() } index++; } - qWarning() << "ComponentList got dataChenged signal from a Component which does not belong to it!"; + qWarning() << "PackProfile got dataChenged signal from a Component which does not belong to it!"; } -bool ComponentList::remove(const int index) +bool PackProfile::remove(const int index) { auto patch = getComponent(index); if (!patch->isRemovable()) @@ -674,7 +679,7 @@ bool ComponentList::remove(const int index) return true; } -bool ComponentList::remove(const QString id) +bool PackProfile::remove(const QString id) { int i = 0; for (auto patch : d->components) @@ -688,7 +693,7 @@ bool ComponentList::remove(const QString id) return false; } -bool ComponentList::customize(int index) +bool PackProfile::customize(int index) { auto patch = getComponent(index); if (!patch->isCustomizable()) @@ -706,7 +711,7 @@ bool ComponentList::customize(int index) return true; } -bool ComponentList::revertToBase(int index) +bool PackProfile::revertToBase(int index) { auto patch = getComponent(index); if (!patch->isRevertible()) @@ -724,7 +729,7 @@ bool ComponentList::revertToBase(int index) return true; } -Component * ComponentList::getComponent(const QString &id) +Component * PackProfile::getComponent(const QString &id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) @@ -734,7 +739,7 @@ Component * ComponentList::getComponent(const QString &id) return (*iter).get(); } -Component * ComponentList::getComponent(int index) +Component * PackProfile::getComponent(int index) { if(index < 0 || index >= d->components.size()) { @@ -743,7 +748,7 @@ Component * ComponentList::getComponent(int index) return d->components[index].get(); } -QVariant ComponentList::data(const QModelIndex &index, int role) const +QVariant PackProfile::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); @@ -762,8 +767,9 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const { switch (column) { - case NameColumn: - return d->components.at(row)->isEnabled() ? Qt::Checked : Qt::Unchecked; + case NameColumn: { + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + } default: return QVariant(); } @@ -773,7 +779,7 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const switch (column) { case NameColumn: - return d->components.at(row)->getName(); + return patch->getName(); case VersionColumn: { if(patch->isCustom()) @@ -816,7 +822,7 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const return QVariant(); } -bool ComponentList::setData(const QModelIndex& index, const QVariant& value, int role) +bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) { @@ -834,7 +840,7 @@ bool ComponentList::setData(const QModelIndex& index, const QVariant& value, int return false; } -QVariant ComponentList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { @@ -853,38 +859,42 @@ QVariant ComponentList::headerData(int section, Qt::Orientation orientation, int } return QVariant(); } -Qt::ItemFlags ComponentList::flags(const QModelIndex &index) const + +// FIXME: zero precision mess +Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const { - if (!index.isValid()) + if (!index.isValid()) { return Qt::NoItemFlags; + } Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; int row = index.row(); - if (row < 0 || row >= d->components.size()) + if (row < 0 || row >= d->components.size()) { return Qt::NoItemFlags; + } auto patch = d->components.at(row); // TODO: this will need fine-tuning later... - if(patch->canBeDisabled()) + if(patch->canBeDisabled() && !d->interactionDisabled) { outFlags |= Qt::ItemIsUserCheckable; } return outFlags; } -int ComponentList::rowCount(const QModelIndex &parent) const +int PackProfile::rowCount(const QModelIndex &parent) const { return d->components.size(); } -int ComponentList::columnCount(const QModelIndex &parent) const +int PackProfile::columnCount(const QModelIndex &parent) const { return NUM_COLUMNS; } -void ComponentList::move(const int index, const MoveDirection direction) +void PackProfile::move(const int index, const MoveDirection direction) { int theirIndex; if (direction == MoveUp) @@ -920,22 +930,22 @@ void ComponentList::move(const int index, const MoveDirection direction) scheduleSave(); } -void ComponentList::invalidateLaunchProfile() +void PackProfile::invalidateLaunchProfile() { d->m_profile.reset(); } -void ComponentList::installJarMods(QStringList selectedFiles) +void PackProfile::installJarMods(QStringList selectedFiles) { installJarMods_internal(selectedFiles); } -void ComponentList::installCustomJar(QString selectedFile) +void PackProfile::installCustomJar(QString selectedFile) { installCustomJar_internal(selectedFile); } -bool ComponentList::installEmpty(const QString& uid, const QString& name) +bool PackProfile::installEmpty(const QString& uid, const QString& name) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) @@ -963,7 +973,7 @@ bool ComponentList::installEmpty(const QString& uid, const QString& name) return true; } -bool ComponentList::removeComponent_internal(ComponentPtr patch) +bool PackProfile::removeComponent_internal(ComponentPtr patch) { bool ok = true; // first, remove the patch file. this ensures it's not used anymore @@ -1013,7 +1023,7 @@ bool ComponentList::removeComponent_internal(ComponentPtr patch) return ok; } -bool ComponentList::installJarMods_internal(QStringList filepaths) +bool PackProfile::installJarMods_internal(QStringList filepaths) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) @@ -1075,7 +1085,7 @@ bool ComponentList::installJarMods_internal(QStringList filepaths) return true; } -bool ComponentList::installCustomJar_internal(QString filepath) +bool PackProfile::installCustomJar_internal(QString filepath) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) @@ -1136,7 +1146,7 @@ bool ComponentList::installCustomJar_internal(QString filepath) return true; } -std::shared_ptr<LaunchProfile> ComponentList::getProfile() const +std::shared_ptr<LaunchProfile> PackProfile::getProfile() const { if(!d->m_profile) { @@ -1158,7 +1168,7 @@ std::shared_ptr<LaunchProfile> ComponentList::getProfile() const return d->m_profile; } -void ComponentList::setOldConfigVersion(const QString& uid, const QString& version) +void PackProfile::setOldConfigVersion(const QString& uid, const QString& version) { if(version.isEmpty()) { @@ -1167,7 +1177,7 @@ void ComponentList::setOldConfigVersion(const QString& uid, const QString& versi d->m_oldConfigVersions[uid] = version; } -bool ComponentList::setComponentVersion(const QString& uid, const QString& version, bool important) +bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) { auto iter = d->componentIndex.find(uid); if(iter != d->componentIndex.end()) @@ -1193,7 +1203,7 @@ bool ComponentList::setComponentVersion(const QString& uid, const QString& versi } } -QString ComponentList::getComponentVersion(const QString& uid) const +QString PackProfile::getComponentVersion(const QString& uid) const { const auto iter = d->componentIndex.find(uid); if (iter != d->componentIndex.end()) @@ -1202,3 +1212,14 @@ QString ComponentList::getComponentVersion(const QString& uid) const } return QString(); } + +void PackProfile::disableInteraction(bool disable) +{ + if(d->interactionDisabled != disable) { + d->interactionDisabled = disable; + auto size = d->components.size(); + if(size) { + emit dataChanged(index(0), index(size - 1)); + } + } +} diff --git a/api/logic/minecraft/ComponentList.h b/api/logic/minecraft/PackProfile.h index 6ddc09eb..e55e6a58 100644 --- a/api/logic/minecraft/ComponentList.h +++ b/api/logic/minecraft/PackProfile.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -31,10 +31,10 @@ #include "net/Mode.h" class MinecraftInstance; -struct ComponentListData; +struct PackProfileData; class ComponentUpdateTask; -class MULTIMC_LOGIC_EXPORT ComponentList : public QAbstractListModel +class MULTIMC_LOGIC_EXPORT PackProfile : public QAbstractListModel { Q_OBJECT friend ComponentUpdateTask; @@ -46,8 +46,8 @@ public: NUM_COLUMNS }; - explicit ComponentList(MinecraftInstance * instance); - virtual ~ComponentList(); + explicit PackProfile(MinecraftInstance * instance); + virtual ~PackProfile(); 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; @@ -104,6 +104,9 @@ public: /// if there is a save scheduled, do it now. void saveNow(); +signals: + void minecraftChanged(); + public: /// get the profile component by id Component * getComponent(const QString &id); @@ -111,6 +114,10 @@ public: /// get the profile component by index Component * getComponent(int index); + /// Add the component to the internal list of patches + // todo(merged): is this the best approach + void appendComponent(ComponentPtr component); + private: void scheduleSave(); bool saveIsScheduled() const; @@ -118,8 +125,6 @@ private: /// apply the component patches. Catches all the errors and returns true/false for success/failure void invalidateLaunchProfile(); - /// Add the component to the internal list of patches - void appendComponent(ComponentPtr component); /// insert component so that its index is ideally the specified one (returns real index) void insertComponent(size_t index, ComponentPtr component); @@ -131,6 +136,7 @@ private slots: void updateSucceeded(); void updateFailed(const QString & error); void componentDataChanged(); + void disableInteraction(bool disable); private: bool load(); @@ -142,5 +148,5 @@ private: private: /* data */ - std::unique_ptr<ComponentListData> d; + std::unique_ptr<PackProfileData> d; }; diff --git a/api/logic/minecraft/ComponentList_p.h b/api/logic/minecraft/PackProfile_p.h index aed65337..6cd2a4e5 100644 --- a/api/logic/minecraft/ComponentList_p.h +++ b/api/logic/minecraft/PackProfile_p.h @@ -9,9 +9,8 @@ class MinecraftInstance; using ComponentContainer = QList<ComponentPtr>; using ComponentIndex = QMap<QString, ComponentPtr>; -using ConnectionList = QList<QMetaObject::Connection>; -struct ComponentListData +struct PackProfileData { // the instance this belongs to MinecraftInstance *m_instance; @@ -38,5 +37,6 @@ struct ComponentListData QTimer m_saveTimer; shared_qobject_ptr<Task> m_updateTask; bool loaded = false; + bool interactionDisabled = true; }; diff --git a/api/logic/minecraft/ParseUtils_test.cpp b/api/logic/minecraft/ParseUtils_test.cpp index fde9cdbf..fcc137e5 100644 --- a/api/logic/minecraft/ParseUtils_test.cpp +++ b/api/logic/minecraft/ParseUtils_test.cpp @@ -33,7 +33,7 @@ slots: auto time_parsed = timeFromS3Time(timestamp); auto time_serialized = timeToS3Time(time_parsed); - + QCOMPARE(time_serialized, timestamp); } diff --git a/api/logic/minecraft/Rule.cpp b/api/logic/minecraft/Rule.cpp index a7640635..af2861e3 100644 --- a/api/logic/minecraft/Rule.cpp +++ b/api/logic/minecraft/Rule.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/Rule.h b/api/logic/minecraft/Rule.h index ec9b26bd..7aa34d96 100644 --- a/api/logic/minecraft/Rule.h +++ b/api/logic/minecraft/Rule.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/SimpleModList.cpp b/api/logic/minecraft/SimpleModList.cpp deleted file mode 100644 index a9fb42eb..00000000 --- a/api/logic/minecraft/SimpleModList.cpp +++ /dev/null @@ -1,367 +0,0 @@ -/* Copyright 2013-2018 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 "SimpleModList.h" -#include <FileSystem.h> -#include <QMimeData> -#include <QUrl> -#include <QUuid> -#include <QString> -#include <QFileSystemWatcher> -#include <QDebug> - -SimpleModList::SimpleModList(const QString &dir) : QAbstractListModel(), m_dir(dir) -{ - FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); - 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 SimpleModList::startWatching() -{ - if(is_watching) - return; - - update(); - - 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(); - } -} - -void SimpleModList::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(); - } -} - -bool SimpleModList::update() -{ - if (!isValid()) - return false; - - QList<Mod> orderedMods; - QList<Mod> newMods; - m_dir.refresh(); - auto folderContents = m_dir.entryInfoList(); - bool orderOrStateChanged = false; - - // if there are any untracked files... - if (folderContents.size()) - { - // the order surely changed! - for (auto entry : folderContents) - { - newMods.append(Mod(entry)); - } - orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } - } - beginResetModel(); - mods.swap(orderedMods); - endResetModel(); - if (orderOrStateChanged) - { - emit changed(); - } - return true; -} - -void SimpleModList::directoryChanged(QString path) -{ - update(); -} - -bool SimpleModList::isValid() -{ - return m_dir.exists() && m_dir.isReadable(); -} - -bool SimpleModList::installMod(const QString &filename) -{ - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - QFileInfo fileinfo(FS::NormalizePath(filename)); - - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - return false; - } - Mod m(fileinfo); - if (!m.valid()) - return false; - - auto type = m.type(); - if (type == Mod::MOD_UNKNOWN) - return false; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName()); - if (!QFile::copy(fileinfo.filePath(), newpath)) - return false; - FS::updateTimestamp(newpath); - m.repath(newpath); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName()); - if (!FS::copy(from, to)()) - return false; - m.repath(to); - update(); - return true; - } - return false; -} - -bool SimpleModList::enableMods(const QModelIndexList& indexes, bool enable) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.enable(enable); - emit dataChanged(i, i); - } - emit changed(); - return true; -} - -bool SimpleModList::deleteMods(const QModelIndexList& indexes) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.destroy(); - } - emit changed(); - return true; -} - -int SimpleModList::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -QVariant SimpleModList::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - 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(); - case VersionColumn: - return mods[row].version(); - case DateColumn: - return mods[row].dateTimeChanged(); - - default: - return QVariant(); - } - - case Qt::ToolTipRole: - return mods[row].mmc_id(); - - case Qt::CheckStateRole: - switch (column) - { - case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } - default: - return QVariant(); - } -} - -bool SimpleModList::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) - { - auto &mod = mods[index.row()]; - if (mod.enable(!mod.enabled())) - { - emit dataChanged(index, index); - return true; - } - } - return false; -} - -QVariant SimpleModList::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 VersionColumn: - return tr("Version"); - case DateColumn: - return tr("Last changed"); - default: - return QVariant(); - } - - case Qt::ToolTipRole: - switch (section) - { - case ActiveColumn: - return tr("Is the mod enabled?"); - case NameColumn: - return tr("The name of the mod."); - case VersionColumn: - return tr("The version of the mod."); - case DateColumn: - return tr("The date and time this mod was last changed (or added)."); - default: - return QVariant(); - } - default: - return QVariant(); - } - return QVariant(); -} - -Qt::ItemFlags SimpleModList::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | - defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; -} - -Qt::DropActions SimpleModList::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList SimpleModList::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool SimpleModList::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()) - { - bool was_watching = is_watching; - if (was_watching) - { - stopWatching(); - } - 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 (was_watching) - { - startWatching(); - } - return true; - } - return false; -} diff --git a/api/logic/minecraft/VersionFile.cpp b/api/logic/minecraft/VersionFile.cpp index cfb9e504..d0a1a507 100644 --- a/api/logic/minecraft/VersionFile.cpp +++ b/api/logic/minecraft/VersionFile.cpp @@ -5,7 +5,7 @@ #include "minecraft/VersionFile.h" #include "minecraft/Library.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "ParseUtils.h" #include <Version.h> @@ -41,6 +41,10 @@ void VersionFile::applyTo(LaunchProfile *profile) { profile->applyLibrary(library); } + for (auto mavenFile : mavenFiles) + { + profile->applyMavenFile(mavenFile); + } profile->applyProblemSeverity(getProblemSeverity()); } @@ -53,4 +57,4 @@ void VersionFile::applyTo(LaunchProfile *profile) throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion); } } -*/
\ No newline at end of file +*/ diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h index 3dc9db96..b79fcd4f 100644 --- a/api/logic/minecraft/VersionFile.h +++ b/api/logic/minecraft/VersionFile.h @@ -12,7 +12,7 @@ #include "Library.h" #include <meta/JsonFormat.h> -class ComponentList; +class PackProfile; class VersionFile; class LaunchProfile; struct MojangDownloadInfo; @@ -75,6 +75,9 @@ public: /* data */ /// Mojang: list of libraries to add to the version QList<LibraryPtr> libraries; + /// MultiMC: list of maven files to put in the libraries folder, but not in classpath + QList<LibraryPtr> mavenFiles; + /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; diff --git a/api/logic/minecraft/VersionFilterData.cpp b/api/logic/minecraft/VersionFilterData.cpp index 11f7eea9..38e7b60c 100644 --- a/api/logic/minecraft/VersionFilterData.cpp +++ b/api/logic/minecraft/VersionFilterData.cpp @@ -7,18 +7,18 @@ VersionFilterData::VersionFilterData() { // 1.3.* auto libs13 = - QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, - {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, - {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}}; + QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}}; fmlLibsMapping["1.3.2"] = libs13; // 1.4.* auto libs14 = QList<FMLlib>{ - {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, - {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, - {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}, - {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", false}}; + {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}, + {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb"}}; fmlLibsMapping["1.4"] = libs14; fmlLibsMapping["1.4.1"] = libs14; @@ -31,30 +31,30 @@ VersionFilterData::VersionFilterData() // 1.5 fmlLibsMapping["1.5"] = QList<FMLlib>{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // 1.5.1 fmlLibsMapping["1.5.1"] = QList<FMLlib>{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // 1.5.2 fmlLibsMapping["1.5.2"] = QList<FMLlib>{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // don't use installers for those. forgeInstallerBlacklist = QSet<QString>({"1.5.2"}); @@ -65,4 +65,7 @@ VersionFilterData::VersionFilterData() QSet<QString>{"net.java.jinput:jinput", "net.java.jinput:jinput-platform", "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"}; + + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); } diff --git a/api/logic/minecraft/VersionFilterData.h b/api/logic/minecraft/VersionFilterData.h index 88e91f11..d100acc3 100644 --- a/api/logic/minecraft/VersionFilterData.h +++ b/api/logic/minecraft/VersionFilterData.h @@ -10,7 +10,6 @@ struct FMLlib { QString filename; QString checksum; - bool ours; }; struct VersionFilterData @@ -24,5 +23,9 @@ struct VersionFilterData QDateTime legacyCutoffDate; // Libraries that belong to LWJGL QSet<QString> lwjglWhitelist; + // release date of first version to require Java 8 (17w13a) + QDateTime java8BeginsDate; + // release data of first version to require Java 16 (21w19a) + QDateTime java16BeginsDate; }; extern VersionFilterData MULTIMC_LOGIC_EXPORT g_VersionFilterData; diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp index b39f940e..a2b4dac7 100644 --- a/api/logic/minecraft/World.cpp +++ b/api/logic/minecraft/World.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -30,6 +30,79 @@ #include <quazipfile.h> #include <quazipdir.h> +#include <QCoreApplication> + +#include <nonstd/optional> + +using nonstd::optional; +using nonstd::nullopt; + +GameType::GameType(nonstd::optional<int> original): + original(original) +{ + if(!original) { + return; + } + switch(*original) { + case 0: + type = GameType::Survival; + break; + case 1: + type = GameType::Creative; + break; + case 2: + type = GameType::Adventure; + break; + case 3: + type = GameType::Spectator; + break; + default: + break; + } +} + +QString GameType::toTranslatedString() const +{ + switch (type) + { + case GameType::Survival: + return QCoreApplication::translate("GameType", "Survival"); + case GameType::Creative: + return QCoreApplication::translate("GameType", "Creative"); + case GameType::Adventure: + return QCoreApplication::translate("GameType", "Adventure"); + case GameType::Spectator: + return QCoreApplication::translate("GameType", "Spectator"); + default: + break; + } + if(original) { + return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); + } + return QCoreApplication::translate("GameType", "Undefined"); +} + +QString GameType::toLogString() const +{ + switch (type) + { + case GameType::Survival: + return "Survival"; + case GameType::Creative: + return "Creative"; + case GameType::Adventure: + return "Adventure"; + case GameType::Spectator: + return "Spectator"; + default: + break; + } + if(original) { + return QString("Unknown (%1)").arg(*original); + } + return "Undefined"; +} + std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data) { QByteArray output; @@ -38,15 +111,22 @@ std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data) return nullptr; } std::istringstream foo(std::string(output.constData(), output.size())); - auto pair = nbt::io::read_compound(foo); + try { + auto pair = nbt::io::read_compound(foo); - if(pair.first != "") - return nullptr; + if(pair.first != "") + return nullptr; - if(pair.second == nullptr) - return nullptr; + if(pair.second == nullptr) + return nullptr; - return std::move(pair.second); + return std::move(pair.second); + } + catch (const nbt::io::input_error &e) + { + qWarning() << "Unable to parse level.dat:" << e.what(); + return nullptr; + } } QByteArray serializeLevelDat(nbt::tag_compound * levelInfo) @@ -118,14 +198,31 @@ void World::repath(const QFileInfo &file) m_folderName = file.fileName(); if(file.isFile() && file.suffix() == "zip") { + m_iconFile = QString(); readFromZip(file); } else if(file.isDir()) { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if(assumedIconPath.exists()) { + m_iconFile = assumedIconPath.absoluteFilePath(); + } readFromFS(file); } } +bool World::resetIcon() +{ + if(m_iconFile.isNull()) { + return false; + } + if(QFile(m_iconFile).remove()) { + m_iconFile = QString(); + return true; + } + return false; +} + void World::readFromFS(const QFileInfo &file) { auto bytes = getLevelDatDataFromFS(file); @@ -192,7 +289,7 @@ bool World::install(const QString &to, const QString &name) { return false; } - ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty(); + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if(m_containerFile.isDir()) { @@ -251,14 +348,16 @@ bool World::rename(const QString &newName) return true; } -static QString read_string (nbt::value& parent, const char * name, const QString & fallback = QString()) +namespace { + +optional<QString> read_string (nbt::value& parent, const char * name) { try { auto &namedValue = parent.at(name); if(namedValue.get_type() != nbt::tag_type::String) { - return fallback; + return nullopt; } auto & tag_str = namedValue.as<nbt::tag_string>(); return QString::fromStdString(tag_str.get()); @@ -266,25 +365,25 @@ static QString read_string (nbt::value& parent, const char * name, const QString catch (const std::out_of_range &e) { // fallback for old world formats - qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback; - return fallback; + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; } catch (const std::bad_cast &e) { // type mismatch - qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback; - return fallback; + qWarning() << "NBT tag" << name << "could not be converted to string."; + return nullopt; } } -static int64_t read_long (nbt::value& parent, const char * name, const int64_t & fallback = 0) +optional<int64_t> read_long (nbt::value& parent, const char * name) { try { auto &namedValue = parent.at(name); if(namedValue.get_type() != nbt::tag_type::Long) { - return fallback; + return nullopt; } auto & tag_str = namedValue.as<nbt::tag_long>(); return tag_str.get(); @@ -292,58 +391,98 @@ static int64_t read_long (nbt::value& parent, const char * name, const int64_t & catch (const std::out_of_range &e) { // fallback for old world formats - qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback; - return fallback; + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; } catch (const std::bad_cast &e) { // type mismatch - qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback; - return fallback; + qWarning() << "NBT tag" << name << "could not be converted to long."; + return nullopt; } } -void World::loadFromLevelDat(QByteArray data) +optional<int> read_int (nbt::value& parent, const char * name) { try { - auto levelData = parseLevelDat(data); - if(!levelData) + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Int) { - is_valid = false; - return; + return nullopt; } + auto & tag_str = namedValue.as<nbt::tag_int>(); + return tag_str.get(); + } + catch (const std::out_of_range &e) + { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } + catch (const std::bad_cast &e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to int."; + return nullopt; + } +} - auto &val = levelData->at("Data"); - is_valid = val.get_type() == nbt::tag_type::Compound; - if(!is_valid) - return; - - m_actualName = read_string(val, "LevelName", m_folderName); - +GameType read_gametype(nbt::value& parent, const char * name) { + return GameType(read_int(parent, name)); +} - int64_t temp = read_long(val, "LastPlayed", 0); - if(temp == 0) - { - m_lastPlayed = levelDatTime; - } - else - { - m_lastPlayed = QDateTime::fromMSecsSinceEpoch(temp); - } +} - m_randomSeed = read_long(val, "RandomSeed", 0); +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if(!levelData) + { + is_valid = false; + return; + } - qDebug() << "World Name:" << m_actualName; - qDebug() << "Last Played:" << m_lastPlayed.toString(); - qDebug() << "Seed:" << m_randomSeed; + nbt::value * valPtr = nullptr; + try { + valPtr = &levelData->at("Data"); } - catch (const nbt::io::input_error &e) - { - qWarning() << "Unable to load" << m_folderName << ":" << e.what(); + catch (const std::out_of_range &e) { + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); is_valid = false; return; } + nbt::value &val = *valPtr; + + is_valid = val.get_type() == nbt::tag_type::Compound; + if(!is_valid) + return; + + auto name = read_string(val, "LevelName"); + m_actualName = name ? *name : m_folderName; + + auto timestamp = read_long(val, "LastPlayed"); + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; + + m_gameType = read_gametype(val, "GameType"); + + optional<int64_t> randomSeed; + try { + auto &WorldGen_val = val.at("WorldGenSettings"); + randomSeed = read_long(WorldGen_val, "seed"); + } + catch (const std::out_of_range &) {} + if(!randomSeed) { + randomSeed = read_long(val, "RandomSeed"); + } + m_randomSeed = randomSeed ? *randomSeed : 0; + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + if(randomSeed) { + qDebug() << "Seed:" << *randomSeed; + } + qDebug() << "GameType:" << m_gameType.toLogString(); } bool World::replace(World &with) @@ -379,7 +518,3 @@ bool World::operator==(const World &other) const { return is_valid == other.is_valid && folderName() == other.folderName(); } -bool World::strongCompare(const World &other) const -{ - return is_valid == other.is_valid && folderName() == other.folderName(); -} diff --git a/api/logic/minecraft/World.h b/api/logic/minecraft/World.h index 2cce85a2..1d94d54d 100644 --- a/api/logic/minecraft/World.h +++ b/api/logic/minecraft/World.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -16,9 +16,28 @@ #pragma once #include <QFileInfo> #include <QDateTime> +#include <nonstd/optional> #include "multimc_logic_export.h" +struct MULTIMC_LOGIC_EXPORT GameType { + GameType() = default; + GameType (nonstd::optional<int> original); + + QString toTranslatedString() const; + QString toLogString() const; + + enum + { + Unknown = -1, + Survival = 0, + Creative, + Adventure, + Spectator + } type = Unknown; + nonstd::optional<int> original; +}; + class MULTIMC_LOGIC_EXPORT World { public: @@ -31,10 +50,18 @@ public: { return m_actualName; } + QString iconFile() const + { + return m_iconFile; + } QDateTime lastPlayed() const { return m_lastPlayed; } + GameType gameType() const + { + return m_gameType; + } int64_t seed() const { return m_randomSeed; @@ -57,13 +84,14 @@ public: bool replace(World &with); // change the world's filesystem path (used by world lists for *MAGIC* purposes) void repath(const QFileInfo &file); + // remove the icon file, if any + bool resetIcon(); bool rename(const QString &to); bool install(const QString &to, const QString &name= QString()); // WEAK compare operator - used for replacing worlds bool operator==(const World &other) const; - bool strongCompare(const World &other) const; private: void readFromZip(const QFileInfo &file); @@ -76,8 +104,10 @@ protected: QString m_containerOffsetPath; QString m_folderName; QString m_actualName; + QString m_iconFile; QDateTime levelDatTime; QDateTime m_lastPlayed; int64_t m_randomSeed = 0; + GameType m_gameType; bool is_valid = false; }; diff --git a/api/logic/minecraft/WorldList.cpp b/api/logic/minecraft/WorldList.cpp index 79a5bf38..f6309dbd 100644 --- a/api/logic/minecraft/WorldList.cpp +++ b/api/logic/minecraft/WorldList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -136,9 +136,22 @@ bool WorldList::deleteWorlds(int first, int last) return true; } +bool WorldList::resetIcon(int row) +{ + if (row >= worlds.size() || row < 0) + return false; + World &m = worlds[row]; + if(m.resetIcon()) { + emit dataChanged(index(row), index(row), {WorldList::IconFileRole}); + return true; + } + return false; +} + + int WorldList::columnCount(const QModelIndex &parent) const { - return 2; + return 3; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -161,6 +174,9 @@ QVariant WorldList::data(const QModelIndex &index, int role) const case NameColumn: return world.name(); + case GameModeColumn: + return world.gameType().toTranslatedString(); + case LastPlayedColumn: return world.lastPlayed(); @@ -192,6 +208,10 @@ QVariant WorldList::data(const QModelIndex &index, int role) const { return world.lastPlayed(); } + case IconFileRole: + { + return world.iconFile(); + } default: return QVariant(); } @@ -206,6 +226,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol { case NameColumn: return tr("Name"); + case GameModeColumn: + return tr("Game Mode"); case LastPlayedColumn: return tr("Last Played"); default: @@ -217,6 +239,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol { case NameColumn: return tr("The name of the world."); + case GameModeColumn: + return tr("Game mode of the world."); case LastPlayedColumn: return tr("Date and time the world was last played."); default: diff --git a/api/logic/minecraft/WorldList.h b/api/logic/minecraft/WorldList.h index a1cd8f51..740b1461 100644 --- a/api/logic/minecraft/WorldList.h +++ b/api/logic/minecraft/WorldList.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -33,6 +33,7 @@ public: enum Columns { NameColumn, + GameModeColumn, LastPlayedColumn }; @@ -42,7 +43,9 @@ public: FolderRole, SeedRole, NameRole, - LastPlayedRole + GameModeRole, + LastPlayedRole, + IconFileRole }; WorldList(const QString &dir); @@ -79,6 +82,9 @@ public: /// Deletes the mod at the given index. virtual bool deleteWorld(int index); + /// Removes the world icon, if any + virtual bool resetIcon(int index); + /// Deletes all the selected mods virtual bool deleteWorlds(int first, int last); diff --git a/api/logic/minecraft/auth/MojangAccount.cpp b/api/logic/minecraft/auth/MojangAccount.cpp index edf1e4e3..f5853fe3 100644 --- a/api/logic/minecraft/auth/MojangAccount.cpp +++ b/api/logic/minecraft/auth/MojangAccount.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * diff --git a/api/logic/minecraft/auth/MojangAccount.h b/api/logic/minecraft/auth/MojangAccount.h index 8f9bec95..30a5f2ff 100644 --- a/api/logic/minecraft/auth/MojangAccount.h +++ b/api/logic/minecraft/auth/MojangAccount.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/MojangAccountList.cpp b/api/logic/minecraft/auth/MojangAccountList.cpp index de671add..e584cb3b 100644 --- a/api/logic/minecraft/auth/MojangAccountList.cpp +++ b/api/logic/minecraft/auth/MojangAccountList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/MojangAccountList.h b/api/logic/minecraft/auth/MojangAccountList.h index 33018636..cc3a61a2 100644 --- a/api/logic/minecraft/auth/MojangAccountList.h +++ b/api/logic/minecraft/auth/MojangAccountList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/YggdrasilTask.cpp b/api/logic/minecraft/auth/YggdrasilTask.cpp index 666e57d6..0857b46b 100644 --- a/api/logic/minecraft/auth/YggdrasilTask.cpp +++ b/api/logic/minecraft/auth/YggdrasilTask.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -25,7 +25,7 @@ #include <Env.h> -#include <net/URLConstants.h> +#include <BuildConfig.h> #include <QDebug> @@ -42,7 +42,7 @@ void YggdrasilTask::executeTask() // Get the content of the request we're going to send to the server. QJsonDocument doc(getRequestContent()); - QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint()); + QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint()); QNetworkRequest netRequest(reqUrl); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -122,7 +122,7 @@ void YggdrasilTask::processReply() tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>" "<ul>" "<li>You use Windows XP and need to <a " - "href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update " + "href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update " "your root certificates</a></li>" "<li>Some device on your network is interfering with SSL traffic. In that case, " "you have bigger worries than Minecraft not starting.</li>" diff --git a/api/logic/minecraft/auth/YggdrasilTask.h b/api/logic/minecraft/auth/YggdrasilTask.h index 4e302195..8af2e132 100644 --- a/api/logic/minecraft/auth/YggdrasilTask.h +++ b/api/logic/minecraft/auth/YggdrasilTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp index 5e37f0ca..2e8dc859 100644 --- a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp @@ -1,5 +1,5 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.h b/api/logic/minecraft/auth/flows/AuthenticateTask.h index 71f0eab7..4c14eec7 100644 --- a/api/logic/minecraft/auth/flows/AuthenticateTask.h +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/RefreshTask.cpp b/api/logic/minecraft/auth/flows/RefreshTask.cpp index f6fe4757..ecba178d 100644 --- a/api/logic/minecraft/auth/flows/RefreshTask.cpp +++ b/api/logic/minecraft/auth/flows/RefreshTask.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/RefreshTask.h b/api/logic/minecraft/auth/flows/RefreshTask.h index ff586396..f0840dda 100644 --- a/api/logic/minecraft/auth/flows/RefreshTask.h +++ b/api/logic/minecraft/auth/flows/RefreshTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/ValidateTask.cpp b/api/logic/minecraft/auth/flows/ValidateTask.cpp index e82678c8..6b3f0a65 100644 --- a/api/logic/minecraft/auth/flows/ValidateTask.cpp +++ b/api/logic/minecraft/auth/flows/ValidateTask.cpp @@ -1,5 +1,5 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/ValidateTask.h b/api/logic/minecraft/auth/flows/ValidateTask.h index 87876fb8..986c2e9f 100644 --- a/api/logic/minecraft/auth/flows/ValidateTask.h +++ b/api/logic/minecraft/auth/flows/ValidateTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/forge/ForgeXzDownload.cpp b/api/logic/minecraft/forge/ForgeXzDownload.cpp deleted file mode 100644 index 4083bdea..00000000 --- a/api/logic/minecraft/forge/ForgeXzDownload.cpp +++ /dev/null @@ -1,393 +0,0 @@ -/* Copyright 2013-2018 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 "Env.h" -#include "ForgeXzDownload.h" -#include <FileSystem.h> - -#include <QCryptographicHash> -#include <QFileInfo> -#include <QDateTime> -#include <QDir> -#include <QDebug> - -ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction() -{ - m_entry = entry; - m_target_path = entry->getFullPath(); - m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX"); - m_status = Job_NotStarted; - m_url_path = relative_path; - m_url = "http://files.minecraftforge.net/maven/" + m_url_path + ".pack.xz"; -} - -void ForgeXzDownload::start() -{ - if(m_status == Job_Aborted) - { - qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); - emit aborted(m_index_within_job); - return; - } - m_status = Job_InProgress; - if (!m_entry->isStale()) - { - m_status = Job_Finished; - emit succeeded(m_index_within_job); - return; - } - // can we actually create the real, final file? - if (!FS::ensureFilePathExists(m_target_path)) - { - m_status = Job_Failed; - emit failed(m_index_within_job); - return; - } - - qDebug() << "Downloading " << m_url.toString(); - QNetworkRequest request(m_url); - request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); - - QNetworkReply *rep = ENV.qnam().get(request); - - m_reply.reset(rep); - connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); - connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); -} - -void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - m_total_progress = bytesTotal; - m_progress = bytesReceived; - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); -} - -void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error) -{ - if(error == QNetworkReply::OperationCanceledError) - { - qCritical() << "Aborted " << m_url.toString(); - m_status = Job_Aborted; - } - else - { - // error happened during download. - qCritical() << "Failed " << m_url.toString() << " with reason " << error; - m_status = Job_Failed; - } -} - -void ForgeXzDownload::failAndTryNextMirror() -{ - m_status = Job_Failed; - emit failed(m_index_within_job); -} - -void ForgeXzDownload::downloadFinished() -{ - // if the download succeeded - if (m_status != Job_Failed && m_status != Job_Aborted) - { - // nothing went wrong... - m_status = Job_Finished; - if (m_pack200_xz_file.isOpen()) - { - // we actually downloaded something! process and isntall it - decompressAndInstall(); - return; - } - else - { - // something bad happened -- on the local machine! - m_status = Job_Failed; - m_pack200_xz_file.remove(); - m_reply.reset(); - emit failed(m_index_within_job); - return; - } - } - else if(m_status == Job_Aborted) - { - m_pack200_xz_file.remove(); - m_reply.reset(); - emit failed(m_index_within_job); - emit aborted(m_index_within_job); - return; - } - // else the download failed - else - { - m_status = Job_Failed; - m_pack200_xz_file.close(); - m_pack200_xz_file.remove(); - m_reply.reset(); - failAndTryNextMirror(); - return; - } -} - -void ForgeXzDownload::downloadReadyRead() -{ - - if (!m_pack200_xz_file.isOpen()) - { - if (!m_pack200_xz_file.open()) - { - /* - * Can't open the file... the job failed - */ - m_reply->abort(); - emit failed(m_index_within_job); - return; - } - } - m_pack200_xz_file.write(m_reply->readAll()); -} - -#include "xz.h" -#include "unpack200.h" -#include <stdexcept> -#include <unistd.h> - -const size_t buffer_size = 8196; - -// NOTE: once this gets here, it can't be aborted anymore. we don't care. -void ForgeXzDownload::decompressAndInstall() -{ - // rewind the downloaded temp file - m_pack200_xz_file.seek(0); - // de-xz'd file - QTemporaryFile pack200_file("./dl_temp.XXXXXX"); - pack200_file.open(); - - bool xz_success = false; - // first, de-xz - { - uint8_t in[buffer_size]; - uint8_t out[buffer_size]; - struct xz_buf b; - struct xz_dec *s; - enum xz_ret ret; - xz_crc32_init(); - xz_crc64_init(); - s = xz_dec_init(XZ_DYNALLOC, 1 << 26); - if (s == nullptr) - { - xz_dec_end(s); - failAndTryNextMirror(); - return; - } - b.in = in; - b.in_pos = 0; - b.in_size = 0; - b.out = out; - b.out_pos = 0; - b.out_size = buffer_size; - while (!xz_success) - { - if (b.in_pos == b.in_size) - { - b.in_size = m_pack200_xz_file.read((char *)in, sizeof(in)); - b.in_pos = 0; - } - - ret = xz_dec_run(s, &b); - - if (b.out_pos == sizeof(out)) - { - auto wresult = pack200_file.write((char *)out, b.out_pos); - if (wresult < 0 || size_t(wresult) != b.out_pos) - { - // msg = "Write error\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - } - - b.out_pos = 0; - } - - if (ret == XZ_OK) - continue; - - if (ret == XZ_UNSUPPORTED_CHECK) - { - // unsupported check. this is OK, but we should log this - continue; - } - - auto wresult = pack200_file.write((char *)out, b.out_pos); - if (wresult < 0 || size_t(wresult) != b.out_pos) - { - // write error - pack200_file.close(); - xz_dec_end(s); - return; - } - - switch (ret) - { - case XZ_STREAM_END: - xz_dec_end(s); - xz_success = true; - break; - - case XZ_MEM_ERROR: - qCritical() << "Memory allocation failed\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_MEMLIMIT_ERROR: - qCritical() << "Memory usage limit reached\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_FORMAT_ERROR: - qCritical() << "Not a .xz file\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_OPTIONS_ERROR: - qCritical() << "Unsupported options in the .xz headers\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_DATA_ERROR: - case XZ_BUF_ERROR: - qCritical() << "File is corrupt\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - default: - qCritical() << "Bug!\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - } - } - } - m_pack200_xz_file.remove(); - - // revert pack200 - pack200_file.seek(0); - int handle_in = pack200_file.handle(); - // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects. - if(handle_in == -1) - { - qCritical() << "Error reopening " << pack200_file.fileName(); - failAndTryNextMirror(); - return; - } - int handle_in_dup = dup (handle_in); - if(handle_in_dup == -1) - { - qCritical() << "Error reopening " << pack200_file.fileName(); - failAndTryNextMirror(); - return; - } - FILE *file_in = fdopen (handle_in_dup, "rb"); - if(!file_in) - { - qCritical() << "Error reopening " << pack200_file.fileName(); - failAndTryNextMirror(); - return; - } - QFile qfile_out(m_target_path); - if(!qfile_out.open(QIODevice::WriteOnly)) - { - qCritical() << "Error opening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - int handle_out = qfile_out.handle(); - if(handle_out == -1) - { - qCritical() << "Error opening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - int handle_out_dup = dup (handle_out); - if(handle_out_dup == -1) - { - qCritical() << "Error reopening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - FILE *file_out = fdopen (handle_out_dup, "wb"); - if(!file_out) - { - qCritical() << "Error opening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - try - { - // NOTE: this takes ownership of both FILE pointers. That's why we duplicate them above. - unpack_200(file_in, file_out); - } - catch (const std::runtime_error &err) - { - m_status = Job_Failed; - qCritical() << "Error unpacking " << pack200_file.fileName() << " : " << err.what(); - QFile f(m_target_path); - if (f.exists()) - f.remove(); - failAndTryNextMirror(); - return; - } - pack200_file.remove(); - - QFile jar_file(m_target_path); - - if (!jar_file.open(QIODevice::ReadOnly)) - { - jar_file.remove(); - failAndTryNextMirror(); - return; - } - auto hash = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5); - m_entry->setMD5Sum(hash.toHex().constData()); - jar_file.close(); - - QFileInfo output_file_info(m_target_path); - m_entry->setETag(m_reply->rawHeader("ETag").constData()); - m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); - m_entry->setStale(false); - ENV.metacache()->updateEntry(m_entry); - - m_reply.reset(); - emit succeeded(m_index_within_job); -} - -bool ForgeXzDownload::abort() -{ - if(m_reply) - m_reply->abort(); - m_status = Job_Aborted; - return true; -} - -bool ForgeXzDownload::canAbort() -{ - return true; -} diff --git a/api/logic/minecraft/forge/ForgeXzDownload.h b/api/logic/minecraft/forge/ForgeXzDownload.h deleted file mode 100644 index 728a7f7a..00000000 --- a/api/logic/minecraft/forge/ForgeXzDownload.h +++ /dev/null @@ -1,61 +0,0 @@ -/* Copyright 2013-2018 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "net/NetAction.h" -#include "net/HttpMetaCache.h" -#include <QFile> -#include <QTemporaryFile> - -typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr; - -class ForgeXzDownload : public NetAction -{ - Q_OBJECT -public: - MetaEntryPtr m_entry; - /// if saving to file, use the one specified in this string - QString m_target_path; - /// this is the output file, if any - QTemporaryFile m_pack200_xz_file; - /// path relative to the mirror base - QString m_url_path; - -public: - explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry); - static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry) - { - return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry)); - } - virtual ~ForgeXzDownload(){}; - bool canAbort() override; - -protected -slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void downloadFinished() override; - void downloadReadyRead() override; - -public -slots: - void start() override; - bool abort() override; - -private: - void decompressAndInstall(); - void failAndTryNextMirror(); -}; diff --git a/api/logic/minecraft/gameoptions/GameOptions.cpp b/api/logic/minecraft/gameoptions/GameOptions.cpp new file mode 100644 index 00000000..e547b32a --- /dev/null +++ b/api/logic/minecraft/gameoptions/GameOptions.cpp @@ -0,0 +1,144 @@ +#include "GameOptions.h" +#include "FileSystem.h" +#include <QDebug> +#include <QSaveFile> + +namespace { +bool load(const QString& path, std::vector<GameOptionItem> &contents, int & version) +{ + contents.clear(); + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + qWarning() << "Failed to read options file."; + return false; + } + version = 0; + while(!file.atEnd()) + { + auto line = file.readLine(); + if(line.endsWith('\n')) + { + line.chop(1); + } + auto separatorIndex = line.indexOf(':'); + if(separatorIndex == -1) + { + continue; + } + auto key = QString::fromUtf8(line.data(), separatorIndex); + auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex); + qDebug() << "!!" << key << "!!"; + if(key == "version") + { + version = value.toInt(); + continue; + } + contents.emplace_back(GameOptionItem{key, value}); + } + qDebug() << "Loaded" << path << "with version:" << version; + return true; +} +bool save(const QString& path, std::vector<GameOptionItem> &mapping, int version) +{ + QSaveFile out(path); + if(!out.open(QIODevice::WriteOnly)) + { + return false; + } + if(version != 0) + { + QString versionLine = QString("version:%1\n").arg(version); + out.write(versionLine.toUtf8()); + } + auto iter = mapping.begin(); + while (iter != mapping.end()) + { + out.write(iter->key.toUtf8()); + out.write(":"); + out.write(iter->value.toUtf8()); + out.write("\n"); + iter++; + } + return out.commit(); +} +} + +GameOptions::GameOptions(const QString& path): + path(path) +{ + reload(); +} + +QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role != Qt::DisplayRole) + { + return QAbstractListModel::headerData(section, orientation, role); + } + switch(section) + { + case 0: + return tr("Key"); + case 1: + return tr("Value"); + default: + return QVariant(); + } +} + +QVariant GameOptions::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= int(contents.size())) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + if(column == 0) + { + return contents[row].key; + } + else + { + return contents[row].value; + } + default: + return QVariant(); + } + return QVariant(); +} + +int GameOptions::rowCount(const QModelIndex&) const +{ + return contents.size(); +} + +int GameOptions::columnCount(const QModelIndex&) const +{ + return 2; +} + +bool GameOptions::isLoaded() const +{ + return loaded; +} + +bool GameOptions::reload() +{ + beginResetModel(); + loaded = load(path, contents, version); + endResetModel(); + return loaded; +} + +bool GameOptions::save() +{ + return ::save(path, contents, version); +} diff --git a/api/logic/minecraft/gameoptions/GameOptions.h b/api/logic/minecraft/gameoptions/GameOptions.h new file mode 100644 index 00000000..c6d25492 --- /dev/null +++ b/api/logic/minecraft/gameoptions/GameOptions.h @@ -0,0 +1,34 @@ +#pragma once + +#include <map> +#include <QString> +#include <QAbstractListModel> + +struct GameOptionItem +{ + QString key; + QString value; +}; + +class GameOptions : public QAbstractListModel +{ + Q_OBJECT +public: + explicit GameOptions(const QString& path); + virtual ~GameOptions() = default; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + bool isLoaded() const; + bool reload(); + bool save(); + +private: + std::vector<GameOptionItem> contents; + bool loaded = false; + QString path; + int version = 0; +}; diff --git a/api/logic/minecraft/launch/ClaimAccount.h b/api/logic/minecraft/launch/ClaimAccount.h index 64891406..c5bd75f3 100644 --- a/api/logic/minecraft/launch/ClaimAccount.h +++ b/api/logic/minecraft/launch/ClaimAccount.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/launch/CreateGameFolders.cpp b/api/logic/minecraft/launch/CreateGameFolders.cpp new file mode 100644 index 00000000..4081e72e --- /dev/null +++ b/api/logic/minecraft/launch/CreateGameFolders.cpp @@ -0,0 +1,28 @@ +#include "CreateGameFolders.h" +#include "minecraft/MinecraftInstance.h" +#include "launch/LaunchTask.h" +#include "FileSystem.h" + +CreateGameFolders::CreateGameFolders(LaunchTask* parent): LaunchStep(parent) +{ +} + +void CreateGameFolders::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); + + if(!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) + { + emit logLine("Couldn't create the main game folder", MessageLevel::Error); + emitFailed(tr("Couldn't create the main game folder")); + return; + } + + // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. + if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) + { + emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); + } + emitSucceeded(); +} diff --git a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.h b/api/logic/minecraft/launch/CreateGameFolders.h index b29496c9..9c7d3c94 100644 --- a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.h +++ b/api/logic/minecraft/launch/CreateGameFolders.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,13 +19,13 @@ #include <LoggedProcess.h> #include <minecraft/auth/AuthSession.h> -// HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. -class CreateServerResourcePacksFolder: public LaunchStep +// Create the main .minecraft for the instance and any other necessary folders +class CreateGameFolders: public LaunchStep { Q_OBJECT public: - explicit CreateServerResourcePacksFolder(LaunchTask *parent); - virtual ~CreateServerResourcePacksFolder() {}; + explicit CreateGameFolders(LaunchTask *parent); + virtual ~CreateGameFolders() {}; virtual void executeTask(); virtual bool canAbort() const diff --git a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.cpp b/api/logic/minecraft/launch/CreateServerResourcePacksFolder.cpp deleted file mode 100644 index ae426e31..00000000 --- a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "CreateServerResourcePacksFolder.h" -#include "minecraft/MinecraftInstance.h" -#include "launch/LaunchTask.h" -#include "FileSystem.h" - -CreateServerResourcePacksFolder::CreateServerResourcePacksFolder(LaunchTask* parent): LaunchStep(parent) -{ -} - -void CreateServerResourcePacksFolder::executeTask() -{ - auto instance = m_parent->instance(); - std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); - if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) - { - emit logLine(tr("Couldn't create the 'server-resource-packs' folder"), MessageLevel::Error); - } - emitSucceeded(); -} diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.cpp b/api/logic/minecraft/launch/DirectJavaLaunch.cpp index 7ead8324..2110384f 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.cpp +++ b/api/logic/minecraft/launch/DirectJavaLaunch.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -55,7 +55,7 @@ void DirectJavaLaunch::executeTask() // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); - auto mcArgs = minecraftInstance->processMinecraftArgs(m_session); + auto mcArgs = minecraftInstance->processMinecraftArgs(m_session, m_serverToJoin); args.append(mcArgs); QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); @@ -66,9 +66,9 @@ void DirectJavaLaunch::executeTask() auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { - QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC); @@ -87,18 +87,17 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) { case LoggedProcess::FailedToStart: { - //: Error message displayed if instace can't start - QString reason = tr("Could not launch minecraft!"); + //: Error message displayed if instance can't start + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: - { m_parent->setPid(-1); - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: @@ -108,7 +107,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) auto exitCode = m_process.exitCode(); if(exitCode != 0) { - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } //FIXME: make this work again @@ -118,7 +117,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) break; } case LoggedProcess::Running: - emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); m_parent->setPid(m_process.processId()); m_parent->instance()->setLastLaunch(); break; diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.h b/api/logic/minecraft/launch/DirectJavaLaunch.h index fb555e3e..58b119b8 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.h +++ b/api/logic/minecraft/launch/DirectJavaLaunch.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,6 +19,8 @@ #include <LoggedProcess.h> #include <minecraft/auth/AuthSession.h> +#include "MinecraftServerTarget.h" + class DirectJavaLaunch: public LaunchStep { Q_OBJECT @@ -38,6 +40,12 @@ public: { m_session = session; } + + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } + private slots: void on_state(LoggedProcess::State state); @@ -45,5 +53,6 @@ private: LoggedProcess m_process; QString m_command; AuthSessionPtr m_session; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/minecraft/launch/ExtractNatives.cpp b/api/logic/minecraft/launch/ExtractNatives.cpp index 336eddbd..d57499aa 100644 --- a/api/logic/minecraft/launch/ExtractNatives.cpp +++ b/api/logic/minecraft/launch/ExtractNatives.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -33,7 +33,7 @@ static QString replaceSuffix (QString target, const QString &suffix, const QStri return target + replacement; } -static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW) { QuaZip zip(source); if(!zip.open(QuaZip::mdUnzip)) @@ -48,6 +48,13 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH do { QString name = zip.getCurrentFileName(); + auto lowercase = name.toLower(); + if (nativeGLFW && name.contains("glfw")) { + continue; + } + if (nativeOpenAL && name.contains("openal")) { + continue; + } if(applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); @@ -76,16 +83,20 @@ void ExtractNatives::executeTask() emitSucceeded(); return; } + auto settings = minecraftInstance->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + auto outputPath = minecraftInstance->getNativePath(); auto javaVersion = minecraftInstance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for(const auto &source: toExtract) { - if(!unzipNatives(source, outputPath, jniHackEnabled)) + if(!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW)) { - auto reason = tr("Couldn't extract native jar '%1' to destination '%2'").arg(source, outputPath); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); + emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); + emitFailed(tr(reason).arg(source, outputPath)); } } emitSucceeded(); diff --git a/api/logic/minecraft/launch/ExtractNatives.h b/api/logic/minecraft/launch/ExtractNatives.h index d9587991..094fcd6b 100644 --- a/api/logic/minecraft/launch/ExtractNatives.h +++ b/api/logic/minecraft/launch/ExtractNatives.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/launch/LauncherPartLaunch.cpp b/api/logic/minecraft/launch/LauncherPartLaunch.cpp index 466c1e46..ee469770 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.cpp +++ b/api/logic/minecraft/launch/LauncherPartLaunch.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -59,7 +59,7 @@ void LauncherPartLaunch::executeTask() auto instance = m_parent->instance(); std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); - m_launchScript = minecraftInstance->createLaunchScript(m_session); + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); QStringList args = minecraftInstance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::MultiMC); @@ -118,9 +118,9 @@ void LauncherPartLaunch::executeTask() auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { - QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC); @@ -140,17 +140,16 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start - QString reason = tr("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: - { m_parent->setPid(-1); - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: @@ -160,7 +159,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) auto exitCode = m_process.exitCode(); if(exitCode != 0) { - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } //FIXME: make this work again @@ -170,7 +169,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) break; } case LoggedProcess::Running: - emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); m_parent->setPid(m_process.processId()); m_parent->instance()->setLastLaunch(); // send the launch script to the launcher part diff --git a/api/logic/minecraft/launch/LauncherPartLaunch.h b/api/logic/minecraft/launch/LauncherPartLaunch.h index 7fadbd66..6a7ee0e5 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.h +++ b/api/logic/minecraft/launch/LauncherPartLaunch.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,6 +19,8 @@ #include <LoggedProcess.h> #include <minecraft/auth/AuthSession.h> +#include "MinecraftServerTarget.h" + class LauncherPartLaunch: public LaunchStep { Q_OBJECT @@ -39,6 +41,11 @@ public: m_session = session; } + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } + private slots: void on_state(LoggedProcess::State state); @@ -47,5 +54,7 @@ private: QString m_command; AuthSessionPtr m_session; QString m_launchScript; + MinecraftServerTargetPtr m_serverToJoin; + bool mayProceed = false; }; diff --git a/api/logic/minecraft/launch/MinecraftServerTarget.cpp b/api/logic/minecraft/launch/MinecraftServerTarget.cpp new file mode 100644 index 00000000..569273b6 --- /dev/null +++ b/api/logic/minecraft/launch/MinecraftServerTarget.cpp @@ -0,0 +1,66 @@ +/* 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 "MinecraftServerTarget.h" + +#include <QStringList> + +MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) { + QStringList split = fullAddress.split(":"); + + // The logic below replicates the exact logic minecraft uses for parsing server addresses. + // While the conversion is not lossless and eats errors, it ensures the same behavior + // within Minecraft and MultiMC when entering server addresses. + if (fullAddress.startsWith("[")) + { + int bracket = fullAddress.indexOf("]"); + if (bracket > 0) + { + QString ipv6 = fullAddress.mid(1, bracket - 1); + QString port = fullAddress.mid(bracket + 1).trimmed(); + + if (port.startsWith(":") && !ipv6.isEmpty()) + { + port = port.mid(1); + split = QStringList({ ipv6, port }); + } + else + { + split = QStringList({ipv6}); + } + } + } + + if (split.size() > 2) + { + split = QStringList({fullAddress}); + } + + QString realAddress = split[0]; + + quint16 realPort = 25565; + if (split.size() > 1) + { + bool ok; + realPort = split[1].toUInt(&ok); + + if (!ok) + { + realPort = 25565; + } + } + + return MinecraftServerTarget { realAddress, realPort }; +} diff --git a/api/logic/minecraft/launch/MinecraftServerTarget.h b/api/logic/minecraft/launch/MinecraftServerTarget.h new file mode 100644 index 00000000..3c5786f4 --- /dev/null +++ b/api/logic/minecraft/launch/MinecraftServerTarget.h @@ -0,0 +1,30 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> + +#include <QString> +#include <multimc_logic_export.h> + +struct MinecraftServerTarget { + QString address; + quint16 port; + + static MULTIMC_LOGIC_EXPORT MinecraftServerTarget parse(const QString &fullAddress); +}; + +typedef std::shared_ptr<MinecraftServerTarget> MinecraftServerTargetPtr; diff --git a/api/logic/minecraft/launch/ModMinecraftJar.cpp b/api/logic/minecraft/launch/ModMinecraftJar.cpp index 34472bb3..93de9d59 100644 --- a/api/logic/minecraft/launch/ModMinecraftJar.cpp +++ b/api/logic/minecraft/launch/ModMinecraftJar.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,7 +19,7 @@ #include "minecraft/OpSys.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" void ModMinecraftJar::executeTask() { @@ -43,7 +43,7 @@ void ModMinecraftJar::executeTask() } // create temporary modded jar, if needed - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto jarMods = m_inst->getJarMods(); if(jarMods.size()) diff --git a/api/logic/minecraft/launch/ModMinecraftJar.h b/api/logic/minecraft/launch/ModMinecraftJar.h index 48e11736..081c6a91 100644 --- a/api/logic/minecraft/launch/ModMinecraftJar.h +++ b/api/logic/minecraft/launch/ModMinecraftJar.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/launch/PrintInstanceInfo.cpp b/api/logic/minecraft/launch/PrintInstanceInfo.cpp index 6d5b93ae..0b9611ad 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.cpp +++ b/api/logic/minecraft/launch/PrintInstanceInfo.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -101,6 +101,6 @@ void PrintInstanceInfo::executeTask() #endif logLines(log, MessageLevel::MultiMC); - logLines(instance->verboseDescription(m_session), MessageLevel::MultiMC); + logLines(instance->verboseDescription(m_session, m_serverToJoin), MessageLevel::MultiMC); emitSucceeded(); } diff --git a/api/logic/minecraft/launch/PrintInstanceInfo.h b/api/logic/minecraft/launch/PrintInstanceInfo.h index ae0a0400..fdc30f31 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.h +++ b/api/logic/minecraft/launch/PrintInstanceInfo.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -18,13 +18,15 @@ #include <launch/LaunchStep.h> #include <memory> #include "minecraft/auth/AuthSession.h" +#include "minecraft/launch/MinecraftServerTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo: public LaunchStep { Q_OBJECT public: - explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session) : LaunchStep(parent), m_session(session) {}; + explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) : + LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin) {}; virtual ~PrintInstanceInfo(){}; virtual void executeTask(); @@ -34,5 +36,6 @@ public: } private: AuthSessionPtr m_session; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/minecraft/launch/ReconstructAssets.cpp b/api/logic/minecraft/launch/ReconstructAssets.cpp new file mode 100644 index 00000000..4d206665 --- /dev/null +++ b/api/logic/minecraft/launch/ReconstructAssets.cpp @@ -0,0 +1,36 @@ +/* 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 "ReconstructAssets.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/AssetsUtils.h" +#include "launch/LaunchTask.h" + +void ReconstructAssets::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); + auto components = minecraftInstance->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + if(!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir())) + { + emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); + } + + emitSucceeded(); +} diff --git a/api/logic/minecraft/launch/ReconstructAssets.h b/api/logic/minecraft/launch/ReconstructAssets.h new file mode 100644 index 00000000..58d7febd --- /dev/null +++ b/api/logic/minecraft/launch/ReconstructAssets.h @@ -0,0 +1,33 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <launch/LaunchStep.h> +#include <memory> + +class ReconstructAssets: public LaunchStep +{ + Q_OBJECT +public: + explicit ReconstructAssets(LaunchTask *parent) : LaunchStep(parent){}; + virtual ~ReconstructAssets(){}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/api/logic/minecraft/launch/ScanModFolders.cpp b/api/logic/minecraft/launch/ScanModFolders.cpp new file mode 100644 index 00000000..2a0e21b3 --- /dev/null +++ b/api/logic/minecraft/launch/ScanModFolders.cpp @@ -0,0 +1,59 @@ +/* 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 "ScanModFolders.h" +#include "launch/LaunchTask.h" +#include "MMCZip.h" +#include "minecraft/OpSys.h" +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/ModFolderModel.h" + +void ScanModFolders::executeTask() +{ + auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance()); + + auto loaders = m_inst->loaderModList(); + connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + if(!loaders->update()) { + m_modsDone = true; + } + + auto cores = m_inst->coreModList(); + connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + if(!cores->update()) { + m_coreModsDone = true; + } + checkDone(); +} + +void ScanModFolders::modsDone() +{ + m_modsDone = true; + checkDone(); +} + +void ScanModFolders::coreModsDone() +{ + m_coreModsDone = true; + checkDone(); +} + +void ScanModFolders::checkDone() +{ + if(m_modsDone && m_coreModsDone) { + emitSucceeded(); + } +} diff --git a/api/logic/minecraft/launch/ScanModFolders.h b/api/logic/minecraft/launch/ScanModFolders.h new file mode 100644 index 00000000..d5989170 --- /dev/null +++ b/api/logic/minecraft/launch/ScanModFolders.h @@ -0,0 +1,42 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <launch/LaunchStep.h> +#include <memory> + +class ScanModFolders: public LaunchStep +{ + Q_OBJECT +public: + explicit ScanModFolders(LaunchTask *parent) : LaunchStep(parent) {}; + virtual ~ScanModFolders(){}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } +private slots: + void coreModsDone(); + void modsDone(); +private: + void checkDone(); + +private: // DATA + bool m_modsDone = false; + bool m_coreModsDone = false; +}; diff --git a/api/logic/minecraft/launch/VerifyJavaInstall.cpp b/api/logic/minecraft/launch/VerifyJavaInstall.cpp new file mode 100644 index 00000000..657669af --- /dev/null +++ b/api/logic/minecraft/launch/VerifyJavaInstall.cpp @@ -0,0 +1,34 @@ +#include "VerifyJavaInstall.h" + +#include <launch/LaunchTask.h> +#include <minecraft/MinecraftInstance.h> +#include <minecraft/PackProfile.h> +#include <minecraft/VersionFilterData.h> + +void VerifyJavaInstall::executeTask() { + auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance()); + + auto javaVersion = m_inst->getJavaVersion(); + auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft"); + + // Java 16 requirement + if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { + if (javaVersion.major() < 16) { + emit logLine("Minecraft 21w19a and above require the use of Java 16", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 21w19a and above require the use of Java 16")); + return; + } + } + // Java 8 requirement + else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate) { + if (javaVersion.major() < 8) { + emit logLine("Minecraft 17w13a and above require the use of Java 8", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 17w13a and above require the use of Java 8")); + return; + } + } + + emitSucceeded(); +} diff --git a/api/logic/minecraft/launch/VerifyJavaInstall.h b/api/logic/minecraft/launch/VerifyJavaInstall.h new file mode 100644 index 00000000..a553106d --- /dev/null +++ b/api/logic/minecraft/launch/VerifyJavaInstall.h @@ -0,0 +1,17 @@ +#pragma once + +#include <launch/LaunchStep.h> + +class VerifyJavaInstall : public LaunchStep { + Q_OBJECT + +public: + explicit VerifyJavaInstall(LaunchTask *parent) : LaunchStep(parent) { + }; + ~VerifyJavaInstall() override = default; + + void executeTask() override; + bool canAbort() const override { + return false; + } +}; diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/api/logic/minecraft/legacy/LegacyInstance.cpp index a35101e3..9f9bda5a 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.cpp +++ b/api/logic/minecraft/legacy/LegacyInstance.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -21,7 +21,6 @@ #include "LegacyInstance.h" #include "minecraft/legacy/LegacyModList.h" -#include "minecraft/SimpleModList.h" #include "minecraft/WorldList.h" #include <MMCZip.h> #include <FileSystem.h> @@ -107,11 +106,6 @@ std::shared_ptr<LegacyModList> LegacyInstance::jarModList() const return jar_mod_list; } -QList<Mod> LegacyInstance::getJarMods() const -{ - return jarModList()->allMods(); -} - QString LegacyInstance::gameRoot() const { QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); @@ -231,7 +225,7 @@ QString LegacyInstance::getStatusbarDescription() return tr("Instance from previous versions."); } -QStringList LegacyInstance::verboseDescription(AuthSessionPtr session) +QStringList LegacyInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QStringList out; diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h index 619e2c83..325bac7a 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -16,11 +16,11 @@ #pragma once #include "BaseInstance.h" -#include "minecraft/Mod.h" +#include "launch/LaunchTask.h" #include "multimc_logic_export.h" -class SimpleModList; +class ModFolderModel; class LegacyModList; class WorldList; class Task; @@ -77,7 +77,6 @@ public: QString customBaseJar() const; std::shared_ptr<LegacyModList> jarModList() const; - QList<Mod> getJarMods() const; std::shared_ptr<WorldList> worldList() const; /*! @@ -112,7 +111,8 @@ public: { return false; } - std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override + shared_qobject_ptr<LaunchTask> createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override { return nullptr; } @@ -126,7 +126,7 @@ public: } QString getStatusbarDescription() override; - QStringList verboseDescription(AuthSessionPtr session) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; QProcessEnvironment createEnvironment() override { diff --git a/api/logic/minecraft/legacy/LegacyModList.cpp b/api/logic/minecraft/legacy/LegacyModList.cpp index ca171b08..7301eb8c 100644 --- a/api/logic/minecraft/legacy/LegacyModList.cpp +++ b/api/logic/minecraft/legacy/LegacyModList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -22,8 +22,7 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file) : m_dir(dir), m_list_file(list_file) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); } @@ -34,15 +33,11 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file) }; typedef QList<OrderItem> OrderList; -static void internalSort(QList<Mod> &what) +static void internalSort(QList<LegacyModList::Mod> &what) { - auto predicate = [](const Mod &left, const Mod &right) + auto predicate = [](const LegacyModList::Mod &left, const LegacyModList::Mod &right) { - if (left.name() == right.name()) - { - return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0; - } - return left.name().localeAwareCompare(right.name()) < 0; + return left.fileName().localeAwareCompare(right.fileName()) < 0; }; std::sort(what.begin(), what.end(), predicate); } @@ -90,7 +85,6 @@ bool LegacyModList::update() QList<Mod> newMods; m_dir.refresh(); auto folderContents = m_dir.entryInfoList(); - bool orderOrStateChanged = false; // first, process the ordered items (if any) OrderList listOrder = readListFile(m_list_file); @@ -124,48 +118,19 @@ bool LegacyModList::update() // remove from the actual folder contents list folderContents.takeAt(idx); // append the new mod - orderedMods.append(Mod(info)); - if (isEnabled != item.enabled) - orderOrStateChanged = true; - } - else - { - orderOrStateChanged = true; + orderedMods.append(info); } } - // if there are any untracked files... + // if there are any untracked files... append them sorted at the end if (folderContents.size()) { - // the order surely changed! for (auto entry : folderContents) { - newMods.append(Mod(entry)); + newMods.append(entry); } internalSort(newMods); orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } } mods.swap(orderedMods); - if (orderOrStateChanged && !m_list_file.isEmpty()) - { - qDebug() << "Mod list " << m_list_file << " changed!"; - } return true; } diff --git a/api/logic/minecraft/legacy/LegacyModList.h b/api/logic/minecraft/legacy/LegacyModList.h index 8bc68b87..8881d471 100644 --- a/api/logic/minecraft/legacy/LegacyModList.h +++ b/api/logic/minecraft/legacy/LegacyModList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,21 +19,14 @@ #include <QString> #include <QDir> -#include "minecraft/Mod.h" - #include "multimc_logic_export.h" -class LegacyInstance; -class BaseInstance; - -/** - * A legacy mod list. - * Backed by a folder. - */ class MULTIMC_LOGIC_EXPORT LegacyModList { public: + using Mod = QFileInfo; + LegacyModList(const QString &dir, const QString &list_file = QString()); /// Reloads the mod list and returns true if the list changed. diff --git a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp index 9a832a24..a4ea60cd 100644 --- a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp +++ b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp @@ -6,7 +6,8 @@ #include <QtConcurrentRun> #include "LegacyInstance.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" +#include "LegacyModList.h" #include "classparser.h" LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance) @@ -83,7 +84,7 @@ void LegacyUpgradeTask::copyFinished() } } } - auto components = inst.getComponentList(); + auto components = inst.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", preferredVersionNumber, true); @@ -96,10 +97,10 @@ void LegacyUpgradeTask::copyFinished() components->installCustomJar(jarPath); } - auto jarMods = legacyInst->getJarMods(); + auto jarMods = legacyInst->jarModList()->allMods(); for(auto & jarMod: jarMods) { - QString modPath = jarMod.filename().absoluteFilePath(); + QString modPath = jarMod.absoluteFilePath(); qDebug() << "jarMod: " << modPath; components->installJarMods({modPath}); } diff --git a/api/logic/minecraft/mod/LocalModParseTask.cpp b/api/logic/minecraft/mod/LocalModParseTask.cpp new file mode 100644 index 00000000..0d6972fb --- /dev/null +++ b/api/logic/minecraft/mod/LocalModParseTask.cpp @@ -0,0 +1,467 @@ +#include "LocalModParseTask.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <quazip.h> +#include <quazipfile.h> +#include <toml.h> + +#include "settings/INIFile.h" +#include "FileSystem.h" + +namespace { + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails> + { + if (!arr.at(0).isObject()) { + return nullptr; + } + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + auto firstObj = arr.at(0).toObject(); + 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->version = firstObj.value("version").toString(); + details->updateurl = firstObj.value("updateUrl").toString(); + auto homeurl = firstObj.value("url").toString().trimmed(); + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + 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? + authors = firstObj.value("authors").toArray(); + } + + for (auto author: authors) + { + details->authors.append(author.toString()); + } + details->credits = firstObj.value("credits").toString(); + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + return getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) { + val = jsonDoc.object().value("modListVersion"); + } + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return nullptr; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) + { + return getInfoFromArray(arrVal.toArray()); + } + } + return nullptr; +} + +// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md +std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + + char errbuf[200]; + // top-level table + toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf)); + + if(!tomlData) + { + return nullptr; + } + + // array defined by [[mods]] + toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); + // we only really care about the first element, since multiple mods in one file is not supported by us at the moment + toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0); + + // mandatory properties - always in [[mods]] + toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); + if(modIdDatum.ok) + { + 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; + free(versionDatum.u.s); + } + toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); + if(displayNameDatum.ok) + { + 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; + free(descriptionDatum.u.s); + } + + // optional properties - can be in the root table or [[mods]] + toml_datum_t authorsDatum = toml_string_in(tomlData, "authors"); + QString authors = ""; + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + else + { + authorsDatum = toml_string_in(tomlModsTable0, "authors"); + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + } + if(!authors.isEmpty()) + { + // author information is stored as a string now, not a list + details->authors.append(authors); + } + // is credits even used anywhere? including this for completion/parity with old data version + toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); + QString credits = ""; + if(creditsDatum.ok) + { + authors = creditsDatum.u.s; + free(creditsDatum.u.s); + } + else + { + creditsDatum = toml_string_in(tomlModsTable0, "credits"); + if(creditsDatum.ok) + { + credits = creditsDatum.u.s; + free(creditsDatum.u.s); + } + } + details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); + QString homeurl = ""; + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + else + { + homeurlDatum = toml_string_in(tomlModsTable0, "displayURL"); + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + } + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + + // this seems to be recursive, so it should free everything + toml_free(tomlData); + + return details; +} + +// https://fabricmc.net/wiki/documentation:fabric_mod_json +std::shared_ptr<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>(); + + 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(); + + if (schemaVersion >= 1) + { + QJsonArray authors = object.value("authors").toArray(); + for (auto author: authors) + { + if(author.isObject()) { + details->authors.append(author.toObject().value("name").toString()); + } + else { + details->authors.append(author.toString()); + } + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details->homeurl = contact.value("homepage").toString(); + } + } + } + return details; +} + +std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + // Read the data + details->name = "Minecraft Forge"; + details->mod_id = "Forge"; + details->homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details->version = major + "." + minor + "." + revision + "." + build; + return details; +} + +std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + 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(); + } + if (object.contains("version")) + { + details->version = object.value("version").toString(""); + } + else + { + details->version = object.value("revision").toString(""); + } + details->mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if(!author.isEmpty()) { + details->authors.append(author); + } + details->description = object.value("description").toString(); + details->homeurl = object.value("url").toString(); + return details; +} + +} + +LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile): + m_token(token), + m_type(type), + m_modFile(modFile), + m_result(new Result()) +{ +} + +void LocalModParseTask::processAsZip() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("META-INF/mods.toml")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModTOML(file.readAll()); + file.close(); + + // to replace ${file.jarVersion} with the actual version, as needed + if (m_result->details && m_result->details->version == "${file.jarVersion}") + { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + // quick and dirty line-by-line parser + auto manifestLines = file.readAll().split('\n'); + QString manifestVersion = ""; + for (auto &line : manifestLines) + { + if (QString(line).startsWith("Implementation-Version: ")) + { + manifestVersion = QString(line).remove("Implementation-Version: "); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF + // also keep with forge's behavior of setting the version to "NONE" if none is found + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") + { + manifestVersion = "NONE"; + } + + m_result->details->version = manifestVersion; + + file.close(); + } + } + + zip.close(); + return; + } + else if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); +} + +void LocalModParseTask::processAsFolder() +{ + QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + m_result->details = ReadMCModInfo(data); + } +} + +void LocalModParseTask::processAsLitemod() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); +} + +void LocalModParseTask::run() +{ + switch(m_type) + { + case Mod::MOD_ZIPFILE: + processAsZip(); + break; + case Mod::MOD_FOLDER: + processAsFolder(); + break; + case Mod::MOD_LITEMOD: + processAsLitemod(); + break; + default: + break; + } + emit finished(m_token); +} diff --git a/api/logic/minecraft/mod/LocalModParseTask.h b/api/logic/minecraft/mod/LocalModParseTask.h new file mode 100644 index 00000000..0f119ba6 --- /dev/null +++ b/api/logic/minecraft/mod/LocalModParseTask.h @@ -0,0 +1,37 @@ +#pragma once +#include <QRunnable> +#include <QDebug> +#include <QObject> +#include "Mod.h" +#include "ModDetails.h" + +class LocalModParseTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QString id; + std::shared_ptr<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(); + +signals: + void finished(int token); + +private: + void processAsZip(); + void processAsFolder(); + void processAsLitemod(); + +private: + int m_token; + Mod::ModType m_type; + QFileInfo m_modFile; + ResultPtr m_result; +}; diff --git a/api/logic/minecraft/mod/Mod.cpp b/api/logic/minecraft/mod/Mod.cpp new file mode 100644 index 00000000..b6bff29b --- /dev/null +++ b/api/logic/minecraft/mod/Mod.cpp @@ -0,0 +1,151 @@ +/* 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 <QDir> +#include <QString> + +#include "Mod.h" +#include <QDebug> +#include <FileSystem.h> + +namespace { + +ModDetails invalidDetails; + +} + + +Mod::Mod(const QFileInfo &file) +{ + repath(file); + m_changedDateTime = file.lastModified(); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + m_mmc_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; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + repath(QFileInfo(path)); + m_enabled = value; + return true; +} + +bool Mod::destroy() +{ + m_type = MOD_UNKNOWN; + return FS::deletePath(m_file.filePath()); +} + + +const ModDetails & Mod::details() const +{ + if(!m_localDetails) + return invalidDetails; + return *m_localDetails; +} + + +QString Mod::version() const +{ + return details().version; +} + +QString Mod::name() const +{ + auto & d = details(); + if(!d.name.isEmpty()) { + return d.name; + } + return m_name; +} + +QString Mod::homeurl() const +{ + return details().homeurl; +} + +QString Mod::description() const +{ + return details().description; +} + +QStringList Mod::authors() const +{ + return details().authors; +} diff --git a/api/logic/minecraft/Mod.h b/api/logic/minecraft/mod/Mod.h index 0c1adf24..f77ffd41 100644 --- a/api/logic/minecraft/Mod.h +++ b/api/logic/minecraft/mod/Mod.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -16,8 +16,16 @@ #pragma once #include <QFileInfo> #include <QDateTime> +#include <QList> +#include <memory> -class Mod +#include "multimc_logic_export.h" + +#include "ModDetails.h" + + + +class MULTIMC_LOGIC_EXPORT Mod { public: enum ModType @@ -29,6 +37,7 @@ public: MOD_LITEMOD, //!< The mod is a litemod }; + Mod() = default; Mod(const QFileInfo &file); QFileInfo filename() const @@ -39,54 +48,14 @@ public: { return m_mmc_id; } - QString mod_id() const - { - return m_mod_id; - } ModType type() const { return m_type; } - QString mcversion() const - { - return m_mcversion; - } - ; bool valid() { return m_type != MOD_UNKNOWN; } - QString name() const - { - QString name = m_name.trimmed(); - if(name.isEmpty() || name == "Example Mod") - { - return m_mmc_id; - } - return m_name; - } - - QString version() const; - - QString homeurl() const - { - return m_homeurl; - } - - QString description() const - { - return m_description; - } - - QString authors() const - { - return m_authors; - } - - QString credits() const - { - return m_credits; - } QDateTime dateTimeChanged() const { @@ -98,45 +67,51 @@ public: return m_enabled; } + const ModDetails &details() const; + + QString name() const; + QString version() const; + QString homeurl() const; + QString description() const; + QStringList authors() const; + bool enable(bool value); // delete all the files of this mod bool destroy(); - // replace this mod with a copy of the other - bool replace(Mod &with); + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); - // WEAK compare operator - used for replacing mods - bool operator==(const Mod &other) const; - bool strongCompare(const Mod &other) const; - -private: - void ReadMCModInfo(QByteArray contents); - void ReadForgeInfo(QByteArray contents); - void ReadLiteModInfo(QByteArray contents); + bool shouldResolve() { + return !m_resolving && !m_resolved; + } + bool isResolving() { + return m_resolving; + } + int resolutionTicket() + { + return m_resolutionTicket; + } + void setResolving(bool resolving, int resolutionTicket) { + m_resolving = resolving; + m_resolutionTicket = resolutionTicket; + } + void finishResolvingWithDetails(std::shared_ptr<ModDetails> details){ + m_resolving = false; + m_resolved = true; + m_localDetails = details; + } protected: - - // FIXME: what do do with those? HMM... - /* - void ReadModInfoData(QString info); - void ReadForgeInfoData(QString infoFileData); - */ - QFileInfo m_file; QDateTime m_changedDateTime; QString m_mmc_id; - QString m_mod_id; - bool m_enabled = true; QString m_name; - QString m_version; - QString m_mcversion; - QString m_homeurl; - QString m_updateurl; - QString m_description; - QString m_authors; - QString m_credits; - - ModType m_type; + bool m_enabled = true; + bool m_resolving = false; + bool m_resolved = false; + int m_resolutionTicket = 0; + ModType m_type = MOD_UNKNOWN; + std::shared_ptr<ModDetails> m_localDetails; }; diff --git a/api/logic/minecraft/mod/ModDetails.h b/api/logic/minecraft/mod/ModDetails.h new file mode 100644 index 00000000..6ab4aee7 --- /dev/null +++ b/api/logic/minecraft/mod/ModDetails.h @@ -0,0 +1,17 @@ +#pragma once + +#include <QString> +#include <QStringList> + +struct ModDetails +{ + QString mod_id; + QString name; + QString version; + QString mcversion; + QString homeurl; + QString updateurl; + QString description; + QStringList authors; + QString credits; +}; diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.cpp b/api/logic/minecraft/mod/ModFolderLoadTask.cpp new file mode 100644 index 00000000..88349877 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderLoadTask.cpp @@ -0,0 +1,18 @@ +#include "ModFolderLoadTask.h" +#include <QDebug> + +ModFolderLoadTask::ModFolderLoadTask(QDir dir) : + m_dir(dir), m_result(new Result()) +{ +} + +void ModFolderLoadTask::run() +{ + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) + { + Mod m(entry); + m_result->mods[m.mmc_id()] = m; + } + emit succeeded(); +} diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.h b/api/logic/minecraft/mod/ModFolderLoadTask.h new file mode 100644 index 00000000..8d720e65 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderLoadTask.h @@ -0,0 +1,29 @@ +#pragma once +#include <QRunnable> +#include <QObject> +#include <QDir> +#include <QMap> +#include "Mod.h" +#include <memory> + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QMap<QString, Mod> mods; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + +public: + ModFolderLoadTask(QDir dir); + void run(); +signals: + void succeeded(); +private: + QDir m_dir; + ResultPtr m_result; +}; diff --git a/api/logic/minecraft/mod/ModFolderModel.cpp b/api/logic/minecraft/mod/ModFolderModel.cpp new file mode 100644 index 00000000..031eebe5 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderModel.cpp @@ -0,0 +1,554 @@ +/* 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 "ModFolderModel.h" +#include <FileSystem.h> +#include <QMimeData> +#include <QUrl> +#include <QUuid> +#include <QString> +#include <QFileSystemWatcher> +#include <QDebug> +#include "ModFolderLoadTask.h" +#include <QThreadPool> +#include <algorithm> +#include "LocalModParseTask.h" + +ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + 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(); + + 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(); + } +} + +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(); + } +} + +bool ModFolderModel::update() +{ + if (!isValid()) { + return false; + } + if(m_update) { + scheduled_update = true; + return true; + } + + auto task = new ModFolderLoadTask(m_dir); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); + threadPool->start(task); + return true; +} + +void ModFolderModel::finishUpdate() +{ + QSet<QString> currentSet = modsIndex.keys().toSet(); + auto & newMods = m_update->mods; + QSet<QString> newSet = newMods.keys().toSet(); + + // 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()); + } + oldMod = 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); + 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.mmc_id()] = idx; + idx++; + } + } + + m_update.reset(); + + emit updateFinished(); + + if(scheduled_update) { + scheduled_update = false; + update(); + } +} + +void ModFolderModel::resolveMod(Mod& m) +{ + if(!m.shouldResolve()) { + return; + } + + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto result = task->result(); + result->id = m.mmc_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(); +} + +// 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); + installedMod.repath(newpath); + 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; + } + installedMod.repath(newpath); + update(); + 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) + { + Mod &m = mods[i.row()]; + m.destroy(); + } + return true; +} + +int ModFolderModel::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + +QVariant ModFolderModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + 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(); + case VersionColumn: { + switch(mods[row].type()) { + case Mod::MOD_FOLDER: + return tr("Folder"); + case Mod::MOD_SINGLEFILE: + return tr("File"); + default: + break; + } + return mods[row].version(); + } + case DateColumn: + return mods[row].dateTimeChanged(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +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.mmc_id(); + if(!mod.enable(!mod.enabled())) { + return false; + } + auto newId = mod.mmc_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) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or added)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) 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; +} + +Qt::DropActions ModFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList ModFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ModFolderModel::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 + installMod(url.toLocalFile()); + } + return true; + } + return false; +} diff --git a/api/logic/minecraft/SimpleModList.h b/api/logic/minecraft/mod/ModFolderModel.h index d623a295..b0a76121 100644 --- a/api/logic/minecraft/SimpleModList.h +++ b/api/logic/minecraft/mod/ModFolderModel.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -16,13 +16,17 @@ #pragma once #include <QList> +#include <QMap> +#include <QSet> #include <QString> #include <QDir> #include <QAbstractListModel> -#include "minecraft/Mod.h" +#include "Mod.h" #include "multimc_logic_export.h" +#include "ModFolderLoadTask.h" +#include "LocalModParseTask.h" class LegacyInstance; class BaseInstance; @@ -32,7 +36,7 @@ class QFileSystemWatcher; * A legacy mod list. * Backed by a folder. */ -class MULTIMC_LOGIC_EXPORT SimpleModList : public QAbstractListModel +class MULTIMC_LOGIC_EXPORT ModFolderModel : public QAbstractListModel { Q_OBJECT public: @@ -40,11 +44,16 @@ public: { ActiveColumn = 0, NameColumn, - DateColumn, VersionColumn, + DateColumn, NUM_COLUMNS }; - SimpleModList(const QString &dir); + enum ModStatusAction { + Disable, + Enable, + Toggle + }; + ModFolderModel(const QString &dir); 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; @@ -59,7 +68,7 @@ public: { return size(); } - ; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; virtual int columnCount(const QModelIndex &parent) const override; @@ -76,9 +85,13 @@ public: { 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. - virtual bool update(); + bool update(); /** * Adds the given mod to the list at the given index - if the list supports custom ordering @@ -86,15 +99,15 @@ public: bool installMod(const QString& filename); /// Deletes all the selected mods - virtual bool deleteMods(const QModelIndexList &indexes); + bool deleteMods(const QModelIndexList &indexes); /// Enable or disable listed mods - virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); + bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); void startWatching(); void stopWatching(); - virtual bool isValid(); + bool isValid(); QDir dir() { @@ -106,16 +119,31 @@ public: return mods; } +public slots: + void disableInteraction(bool disabled); + private slots: void directoryChanged(QString path); + void finishUpdate(); + void finishModParse(int token); signals: - void changed(); + void updateFinished(); + +private: + void resolveMod(Mod& m); + bool setModStatus(int index, ModStatusAction action); protected: QFileSystemWatcher *m_watcher; bool is_watching = false; + ModFolderLoadTask::ResultPtr m_update; + bool scheduled_update = false; + bool interaction_disabled = false; QDir m_dir; + QMap<QString, int> modsIndex; + QMap<int, LocalModParseTask::ResultPtr> activeTickets; + int nextResolutionTicket = 0; QList<Mod> mods; }; diff --git a/api/logic/minecraft/SimpleModList_test.cpp b/api/logic/minecraft/mod/ModFolderModel_test.cpp index a100b539..76f16ed5 100644 --- a/api/logic/minecraft/SimpleModList_test.cpp +++ b/api/logic/minecraft/mod/ModFolderModel_test.cpp @@ -4,9 +4,9 @@ #include "TestUtil.h" #include "FileSystem.h" -#include "minecraft/SimpleModList.h" +#include "minecraft/mod/ModFolderModel.h" -class SimpleModListTest : public QObject +class ModFolderModelTest : public QObject { Q_OBJECT @@ -32,7 +32,7 @@ slots: { QString folder = source; QTemporaryDir tempDir; - SimpleModList m(tempDir.path()); + ModFolderModel m(tempDir.path()); m.installMod(folder); verify(tempDir.path()); } @@ -41,13 +41,13 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; - SimpleModList m(tempDir.path()); + ModFolderModel m(tempDir.path()); m.installMod(folder); verify(tempDir.path()); } } }; -QTEST_GUILESS_MAIN(SimpleModListTest) +QTEST_GUILESS_MAIN(ModFolderModelTest) -#include "SimpleModList_test.moc" +#include "ModFolderModel_test.moc" diff --git a/api/logic/minecraft/mod/ResourcePackFolderModel.cpp b/api/logic/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 00000000..f3d7f566 --- /dev/null +++ b/api/logic/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "ResourcePackFolderModel.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +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(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/api/logic/minecraft/mod/ResourcePackFolderModel.h b/api/logic/minecraft/mod/ResourcePackFolderModel.h new file mode 100644 index 00000000..47eb4bb2 --- /dev/null +++ b/api/logic/minecraft/mod/ResourcePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class MULTIMC_LOGIC_EXPORT ResourcePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit ResourcePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/api/logic/minecraft/mod/TexturePackFolderModel.cpp b/api/logic/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 00000000..d5956da1 --- /dev/null +++ b/api/logic/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,23 @@ +#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); +} diff --git a/api/logic/minecraft/mod/TexturePackFolderModel.h b/api/logic/minecraft/mod/TexturePackFolderModel.h new file mode 100644 index 00000000..d773b17b --- /dev/null +++ b/api/logic/minecraft/mod/TexturePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class MULTIMC_LOGIC_EXPORT TexturePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit TexturePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/api/logic/minecraft/services/SkinDelete.cpp b/api/logic/minecraft/services/SkinDelete.cpp new file mode 100644 index 00000000..34977257 --- /dev/null +++ b/api/logic/minecraft/services/SkinDelete.cpp @@ -0,0 +1,42 @@ +#include "SkinDelete.h" +#include <QNetworkRequest> +#include <QHttpMultiPart> +#include <Env.h> + +SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session) + : Task(parent), m_session(session) +{ +} + +void SkinDelete::executeTask() +{ + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + m_reply = std::shared_ptr<QNetworkReply>(rep); + + setStatus(tr("Deleting skin")); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void SkinDelete::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void SkinDelete::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + diff --git a/api/logic/minecraft/services/SkinDelete.h b/api/logic/minecraft/services/SkinDelete.h new file mode 100644 index 00000000..705ce8ef --- /dev/null +++ b/api/logic/minecraft/services/SkinDelete.h @@ -0,0 +1,30 @@ +#pragma once + +#include <QFile> +#include <QtNetwork/QtNetwork> +#include <memory> +#include <minecraft/auth/AuthSession.h> +#include "tasks/Task.h" +#include "multimc_logic_export.h" + +typedef std::shared_ptr<class SkinDelete> SkinDeletePtr; + +class MULTIMC_LOGIC_EXPORT SkinDelete : public Task +{ + Q_OBJECT +public: + SkinDelete(QObject *parent, AuthSessionPtr session); + virtual ~SkinDelete() = default; + +private: + AuthSessionPtr m_session; + std::shared_ptr<QNetworkReply> m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + diff --git a/api/logic/minecraft/SkinUpload.cpp b/api/logic/minecraft/services/SkinUpload.cpp index 83bdf592..4e5a1698 100644 --- a/api/logic/minecraft/SkinUpload.cpp +++ b/api/logic/minecraft/services/SkinUpload.cpp @@ -3,15 +3,14 @@ #include <QHttpMultiPart> #include <Env.h> -QByteArray getModelString(SkinUpload::Model model) { +QByteArray getVariant(SkinUpload::Model model) { switch (model) { - case SkinUpload::STEVE: - return ""; - case SkinUpload::ALEX: - return "slim"; default: qDebug() << "Unknown skin type!"; - return ""; + case SkinUpload::STEVE: + return "CLASSIC"; + case SkinUpload::ALEX: + return "SLIM"; } } @@ -22,25 +21,23 @@ SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, void SkinUpload::executeTask() { - QNetworkRequest request(QUrl(QString("https://api.mojang.com/user/profile/%1/skin").arg(m_session->uuid))); - request.setRawHeader("Authorization", QString("Bearer: %1").arg(m_session->access_token).toLocal8Bit()); - + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - QHttpPart model; - model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"model\"")); - model.setBody(getModelString(m_model)); - QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); - skin.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); skin.setBody(m_skin); - multiPart->append(model); + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(getVariant(m_model)); + multiPart->append(skin); + multiPart->append(model); - QNetworkReply *rep = ENV.qnam().put(request, multiPart); + QNetworkReply *rep = ENV.qnam().post(request, multiPart); m_reply = std::shared_ptr<QNetworkReply>(rep); setStatus(tr("Uploading skin")); diff --git a/api/logic/minecraft/SkinUpload.h b/api/logic/minecraft/services/SkinUpload.h index c77abb03..c77abb03 100644 --- a/api/logic/minecraft/SkinUpload.h +++ b/api/logic/minecraft/services/SkinUpload.h diff --git a/api/logic/minecraft/update/AssetUpdateTask.cpp b/api/logic/minecraft/update/AssetUpdateTask.cpp index 1661822d..e26ab4ef 100644 --- a/api/logic/minecraft/update/AssetUpdateTask.cpp +++ b/api/logic/minecraft/update/AssetUpdateTask.cpp @@ -1,7 +1,7 @@ #include "Env.h" #include "AssetUpdateTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "net/ChecksumValidator.h" #include "minecraft/AssetsUtils.h" @@ -17,7 +17,7 @@ AssetUpdateTask::~AssetUpdateTask() void AssetUpdateTask::executeTask() { setStatus(tr("Updating assets index...")); - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; @@ -54,13 +54,13 @@ void AssetUpdateTask::assetIndexFinished() AssetsIndex index; qDebug() << m_inst->name() << ": Finished asset index download"; - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QString asset_fname = "assets/indexes/" + assets->id + ".json"; // FIXME: this looks like a job for a generic validator based on json schema? - if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, &index)) + if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) { auto metacache = ENV.metacache(); auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); @@ -101,7 +101,7 @@ bool AssetUpdateTask::abort() } else { - qWarning() << "Prematurely aborted FMLLibrariesTask"; + qWarning() << "Prematurely aborted AssetUpdateTask"; } return true; } diff --git a/api/logic/minecraft/update/FMLLibrariesTask.cpp b/api/logic/minecraft/update/FMLLibrariesTask.cpp index 5b4975ab..a05a7c2a 100644 --- a/api/logic/minecraft/update/FMLLibrariesTask.cpp +++ b/api/logic/minecraft/update/FMLLibrariesTask.cpp @@ -3,7 +3,8 @@ #include <minecraft/VersionFilterData.h> #include "FMLLibrariesTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" +#include "BuildConfig.h" FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance * inst) { @@ -13,7 +14,7 @@ void FMLLibrariesTask::executeTask() { // Get the mod list MinecraftInstance *inst = (MinecraftInstance *)m_inst; - auto components = inst->getComponentList(); + auto components = inst->getPackProfile(); auto profile = components->getProfile(); if (!profile->hasTrait("legacyFML")) @@ -63,8 +64,7 @@ void FMLLibrariesTask::executeTask() for (auto &lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename - : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; + QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry)); } diff --git a/api/logic/minecraft/update/LibrariesTask.cpp b/api/logic/minecraft/update/LibrariesTask.cpp index 6dcb149c..7f66a651 100644 --- a/api/logic/minecraft/update/LibrariesTask.cpp +++ b/api/logic/minecraft/update/LibrariesTask.cpp @@ -1,7 +1,7 @@ #include "Env.h" #include "LibrariesTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" LibrariesTask::LibrariesTask(MinecraftInstance * inst) { @@ -15,51 +15,51 @@ void LibrariesTask::executeTask() MinecraftInstance *inst = (MinecraftInstance *)m_inst; // Build a list of URLs that will need to be downloaded. - auto components = inst->getComponentList(); + auto components = inst->getPackProfile(); auto profile = components->getProfile(); auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); downloadJob.reset(job); auto metacache = ENV.metacache(); - QList<LibraryPtr> brokenLocalLibs; - QStringList failedFiles; - auto createJob = [&](const LibraryPtr & lib) - { - if(!lib) - { - emitFailed(tr("Null jar is specified in the metadata, aborting.")); - return; - } - auto dls = lib->getDownloads(currentSystem, metacache.get(), failedFiles, inst->getLocalLibraryPath()); - for(auto dl : dls) - { - downloadJob->addNetAction(dl); - } - }; - auto createJobs = [&](const QList<LibraryPtr> & libs) + + auto processArtifactPool = [&](const QList<LibraryPtr> & pool, QStringList & errors, const QString & localPath) { - for (auto lib : libs) + for (auto lib : pool) { - createJob(lib); + if(!lib) + { + emitFailed(tr("Null jar is specified in the metadata, aborting.")); + return false; + } + auto dls = lib->getDownloads(currentSystem, metacache.get(), errors, localPath); + for(auto dl : dls) + { + downloadJob->addNetAction(dl); + } } + return true; }; - createJobs(profile->getLibraries()); - createJobs(profile->getNativeLibraries()); - createJobs(profile->getJarMods()); - createJob(profile->getMainJar()); - // FIXME: this is never filled!!!! - if (!brokenLocalLibs.empty()) + QStringList failedLocalLibraries; + QList<LibraryPtr> libArtifactPool; + libArtifactPool.append(profile->getLibraries()); + libArtifactPool.append(profile->getNativeLibraries()); + libArtifactPool.append(profile->getMavenFiles()); + libArtifactPool.append(profile->getMainJar()); + processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); + + QStringList failedLocalJarMods; + processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); + + if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) { downloadJob.reset(); - QString failed_all = failedFiles.join("\n"); - emitFailed(tr("Some libraries marked as 'local' are missing their jar " - "files:\n%1\n\nYou'll have to correct this problem manually. If this is " - "an externally tracked instance, make sure to run it at least once " - "outside of MultiMC.").arg(failed_all)); + QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); + emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the files, or removed the packages that require them.\nYou'll have to correct this problem manually.").arg(failed_all)); return; } + connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.cpp b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp new file mode 100644 index 00000000..35f50b18 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp @@ -0,0 +1,33 @@ +#include "ATLPackIndex.h" + +#include <QRegularExpression> + +#include "Json.h" + +static void loadIndexedVersion(ATLauncher::IndexedVersion & v, QJsonObject & obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); +} + +void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack & m, QJsonObject & obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.position = Json::requireInteger(obj, "position"); + m.name = Json::requireString(obj, "name"); + m.type = Json::requireString(obj, "type") == "private" ? + ATLauncher::PackType::Private : + ATLauncher::PackType::Public; + auto versionsArr = Json::requireArray(obj, "versions"); + for (const auto versionRaw : versionsArr) + { + auto versionObj = Json::requireObject(versionRaw); + ATLauncher::IndexedVersion version; + loadIndexedVersion(version, versionObj); + m.versions.append(version); + } + m.system = Json::ensureBoolean(obj, QString("system"), false); + m.description = Json::ensureString(obj, "description", ""); + + m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), ""); +} diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.h b/api/logic/modplatform/atlauncher/ATLPackIndex.h new file mode 100644 index 00000000..5e2e6487 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackIndex.h @@ -0,0 +1,36 @@ +#pragma once + +#include "ATLPackManifest.h" + +#include <QString> +#include <QVector> +#include <QMetaType> + +#include "multimc_logic_export.h" + +namespace ATLauncher +{ + +struct IndexedVersion +{ + QString version; + QString minecraft; +}; + +struct IndexedPack +{ + int id; + int position; + QString name; + PackType type; + QVector<IndexedVersion> versions; + bool system; + QString description; + + QString safeName; +}; + +MULTIMC_LOGIC_EXPORT void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +} + +Q_DECLARE_METATYPE(ATLauncher::IndexedPack) diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp new file mode 100644 index 00000000..55087a27 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -0,0 +1,764 @@ +#include <Env.h> +#include <quazip.h> +#include <QtConcurrent/QtConcurrent> +#include <MMCZip.h> +#include <minecraft/OneSixVersionFormat.h> +#include <Version.h> +#include <net/ChecksumValidator.h> +#include "ATLPackInstallTask.h" + +#include "BuildConfig.h" +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" +#include "meta/Index.h" +#include "meta/Version.h" +#include "meta/VersionList.h" + +namespace ATLauncher { + +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) +{ + m_support = support; + m_pack = pack; + m_version_name = version; +} + +bool PackInstallTask::abort() +{ + if(abortable) + { + return jobPtr->abort(); + } + return false; +} + +void PackInstallTask::executeTask() +{ + qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); + auto *netJob = new NetJob("ATLauncher::VersionFetch"); + auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") + .arg(m_pack).arg(m_version_name); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); +} + +void PackInstallTask::onDownloadSucceeded() +{ + qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); + 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 FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + ATLauncher::PackVersion version; + try + { + ATLauncher::loadVersion(version, obj); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + m_version = version; + + auto vlist = ENV.metadataIndex()->get("net.minecraft"); + if(!vlist) + { + emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft")); + return; + } + + auto ver = vlist->getVersion(m_version.minecraft); + if (!ver) { + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft)); + return; + } + ver->load(Net::Mode::Online); + minecraftVersion = ver; + + if(m_version.noConfigs) { + downloadMods(); + } + else { + installConfigs(); + } +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId(); + jobPtr.reset(); + emitFailed(reason); +} + +QString PackInstallTask::getDirForModType(ModType type, QString raw) +{ + switch (type) { + // Mod types that can either be ignored at this stage, or ignored + // completely. + case ModType::Root: + case ModType::Extract: + case ModType::Decomp: + case ModType::TexturePackExtract: + case ModType::ResourcePackExtract: + case ModType::MCPC: + return Q_NULLPTR; + case ModType::Forge: + // Forge detection happens later on, if it cannot be detected it will + // install a jarmod component. + case ModType::Jar: + return "jarmods"; + case ModType::Mods: + return "mods"; + case ModType::Flan: + return "Flan"; + case ModType::Dependency: + return FS::PathCombine("mods", m_version.minecraft); + case ModType::Ic2Lib: + return FS::PathCombine("mods", "ic2"); + case ModType::DenLib: + return FS::PathCombine("mods", "denlib"); + case ModType::Coremods: + return "coremods"; + case ModType::Plugins: + return "plugins"; + case ModType::TexturePack: + return "texturepacks"; + case ModType::ResourcePack: + return "resourcepacks"; + case ModType::ShaderPack: + return "shaderpacks"; + case ModType::Millenaire: + qWarning() << "Unsupported mod type: " + raw; + return Q_NULLPTR; + case ModType::Unknown: + emitFailed(tr("Unknown mod type: %1").arg(raw)); + return Q_NULLPTR; + } + + return Q_NULLPTR; +} + +QString PackInstallTask::getVersionForLoader(QString uid) +{ + if(m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { + auto vlist = ENV.metadataIndex()->get(uid); + if(!vlist) + { + emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); + return Q_NULLPTR; + } + + if(!vlist->isLoaded()) { + vlist->load(Net::Mode::Online); + } + + if(m_version.loader.recommended || m_version.loader.latest) { + 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. + // not all mod loaders depend on a given Minecraft version, so we won't do this + // filtering for those loaders. + if (m_version.loader.type != "fabric") { + 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 != m_version.minecraft) continue; + } + + if (m_version.loader.recommended) { + // first recommended build we find, we use. + if (!version->isRecommended()) continue; + } + + return version->descriptor(); + } + + emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); + return Q_NULLPTR; + } + else if(m_version.loader.choose) { + // Fabric Loader doesn't depend on a given Minecraft version. + if (m_version.loader.type == "fabric") { + return m_support->chooseVersion(vlist, Q_NULLPTR); + } + + return m_support->chooseVersion(vlist, m_version.minecraft); + } + } + + if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) { + emitFailed(tr("No loader version set for modpack!")); + return Q_NULLPTR; + } + + return m_version.loader.version; +} + +QString PackInstallTask::detectLibrary(VersionLibrary library) +{ + // Try to detect what the library is + if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { + auto lastSlash = library.server.lastIndexOf("/"); + auto locationAndVersion = library.server.mid(0, lastSlash); + auto fileName = library.server.mid(lastSlash + 1); + + lastSlash = locationAndVersion.lastIndexOf("/"); + auto location = locationAndVersion.mid(0, lastSlash); + auto version = locationAndVersion.mid(lastSlash + 1); + + lastSlash = location.lastIndexOf("/"); + auto group = location.mid(0, lastSlash).replace("/", "."); + auto artefact = location.mid(lastSlash + 1); + + return group + ":" + artefact + ":" + version; + } + + if(library.file.contains("-")) { + auto lastSlash = library.file.lastIndexOf("-"); + auto name = library.file.mid(0, lastSlash); + auto version = library.file.mid(lastSlash + 1).remove(".jar"); + + if(name == QString("guava")) { + return "com.google.guava:guava:" + version; + } + else if(name == QString("commons-lang3")) { + return "org.apache.commons:commons-lang3:" + version; + } + } + + return "org.multimc.atlauncher:" + library.md5 + ":1"; +} + +bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile) +{ + if(m_version.libraries.isEmpty()) { + return true; + } + + QList<GradleSpecifier> exempt; + for(const auto & componentUid : componentsToInstall.keys()) { + auto componentVersion = componentsToInstall.value(componentUid); + + for(const auto & library : componentVersion->data()->libraries) { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } + } + + { + for(const auto & library : minecraftVersion->data()->libraries) { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } + } + + auto uuid = QUuid::createUuid(); + auto id = uuid.toString().remove('{').remove('}'); + auto target_id = "org.multimc.atlauncher." + id; + + auto patchDir = FS::PathCombine(instanceRoot, "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + auto f = std::make_shared<VersionFile>(); + f->name = m_pack + " " + m_version_name + " (libraries)"; + + for(const auto & lib : m_version.libraries) { + auto libName = detectLibrary(lib); + GradleSpecifier libSpecifier(libName); + + bool libExempt = false; + for(const auto & existingLib : exempt) { + if(libSpecifier.matchName(existingLib)) { + // If the pack specifies a newer version of the lib, use that! + libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); + } + } + if(libExempt) continue; + + auto library = std::make_shared<Library>(); + library->setRawName(libName); + + switch(lib.download) { + case DownloadType::Server: + library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url); + break; + case DownloadType::Direct: + library->setAbsoluteUrl(lib.url); + break; + case DownloadType::Browser: + case DownloadType::Unknown: + emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw)); + return false; + } + + f->libraries.append(library); + } + + if(f->libraries.isEmpty()) { + return true; + } + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + profile->appendComponent(new Component(profile.get(), target_id, f)); + return true; +} + +bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile) +{ + if(m_version.mainClass == QString() && m_version.extraArguments == QString()) { + return true; + } + + auto uuid = QUuid::createUuid(); + auto id = uuid.toString().remove('{').remove('}'); + auto target_id = "org.multimc.atlauncher." + id; + + auto patchDir = FS::PathCombine(instanceRoot, "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QStringList mainClasses; + QStringList tweakers; + for(const auto & componentUid : componentsToInstall.keys()) { + auto componentVersion = componentsToInstall.value(componentUid); + + if(componentVersion->data()->mainClass != QString("")) { + mainClasses.append(componentVersion->data()->mainClass); + } + tweakers.append(componentVersion->data()->addTweakers); + } + + auto f = std::make_shared<VersionFile>(); + f->name = m_pack + " " + m_version_name; + if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) { + f->mainClass = m_version.mainClass; + } + + // Parse out tweakers + auto args = m_version.extraArguments.split(" "); + QString previous; + for(auto arg : args) { + if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") { + auto tweakClass = arg.remove("--tweakClass="); + if(tweakers.contains(tweakClass)) continue; + + f->addTweakers.append(tweakClass); + } + previous = arg; + } + + if(f->mainClass == QString() && f->addTweakers.isEmpty()) { + return true; + } + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + profile->appendComponent(new Component(profile.get(), target_id, f)); + return true; +} + +void PackInstallTask::installConfigs() +{ + qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); + setStatus(tr("Downloading configs...")); + jobPtr.reset(new NetJob(tr("Config download"))); + + auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") + .arg(m_pack).arg(m_version_name); + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path); + entry->setStale(true); + + auto dl = Net::Download::makeCached(url, entry); + if (!m_version.configs.sha1.isEmpty()) { + auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + } + jobPtr->addNetAction(dl); + archivePath = entry->getFullPath(); + + connect(jobPtr.get(), &NetJob::succeeded, this, [&]() + { + abortable = false; + jobPtr.reset(); + extractConfigs(); + }); + 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); + }); + + jobPtr->start(); +} + +void PackInstallTask::extractConfigs() +{ + qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId(); + setStatus(tr("Extracting configs...")); + + QDir extractDir(m_stagingPath); + + QuaZip packZip(archivePath); + if(!packZip.open(QuaZip::mdUnzip)) + { + emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); + return; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [&]() + { + downloadMods(); + }); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]() + { + emitAborted(); + }); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void PackInstallTask::downloadMods() +{ + qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); + + QVector<ATLauncher::VersionMod> optionalMods; + for (const auto& mod : m_version.mods) { + if (mod.optional) { + optionalMods.push_back(mod); + } + } + + // Select optional mods, if pack contains any + QVector<QString> selectedMods; + if (!optionalMods.isEmpty()) { + setStatus(tr("Selecting optional mods...")); + selectedMods = m_support->chooseOptionalMods(optionalMods); + } + + setStatus(tr("Downloading mods...")); + + jarmods.clear(); + jobPtr.reset(new NetJob(tr("Mod download"))); + for(const auto& mod : m_version.mods) { + // skip non-client mods + if(!mod.client) continue; + + // skip optional mods that were not selected + if(mod.optional && !selectedMods.contains(mod.name)) continue; + + QString url; + switch(mod.download) { + case DownloadType::Server: + url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; + break; + case DownloadType::Browser: + emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw)); + return; + case DownloadType::Direct: + url = mod.url; + break; + case DownloadType::Unknown: + emitFailed(tr("Unknown download type: %1").arg(mod.download_raw)); + return; + } + + QFileInfo fileName(mod.file); + auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); + + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToExtract.insert(entry->getFullPath(), mod); + + auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + jobPtr->addNetAction(dl); + } + else if(mod.type == ModType::Decomp) { + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToDecomp.insert(entry->getFullPath(), mod); + + auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + jobPtr->addNetAction(dl); + } + else { + auto relpath = getDirForModType(mod.type, mod.type_raw); + if(relpath == Q_NULLPTR) continue; + + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + + auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + jobPtr->addNetAction(dl); + + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; + + if(mod.type == ModType::Forge) { + auto vlist = ENV.metadataIndex()->get("net.minecraftforge"); + if(vlist) + { + auto ver = vlist->getVersion(mod.version); + if(ver) { + ver->load(Net::Mode::Online); + componentsToInstall.insert("net.minecraftforge", ver); + continue; + } + } + + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + if(mod.type == ModType::Jar) { + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + } + } + + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); + 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); + }); + + jobPtr->start(); +} + +void PackInstallTask::onModsDownloaded() { + abortable = false; + + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); + jobPtr.reset(); + + if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { + m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); + connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted); + connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]() + { + emitAborted(); + }); + m_modExtractFutureWatcher.setFuture(m_modExtractFuture); + } + else { + install(); + } +} + +void PackInstallTask::onModsExtracted() { + qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId(); + if(m_modExtractFuture.result()) { + install(); + } + else { + emitFailed(tr("Failed to extract mods...")); + } +} + +bool PackInstallTask::extractMods( + const QMap<QString, VersionMod> &toExtract, + const QMap<QString, VersionMod> &toDecomp, + const QMap<QString, QString> &toCopy +) { + qDebug() << "PackInstallTask::extractMods: " << QThread::currentThreadId(); + + setStatus(tr("Extracting mods...")); + for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + + QString extractToDir; + if(mod.type == ModType::Extract) { + extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); + } + else if(mod.type == ModType::TexturePackExtract) { + extractToDir = FS::PathCombine("texturepacks", "extracted"); + } + else if(mod.type == ModType::ResourcePackExtract) { + extractToDir = FS::PathCombine("resourcepacks", "extracted"); + } + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); + + QString folderToExtract = ""; + if(mod.type == ModType::Extract) { + folderToExtract = mod.extractFolder; + folderToExtract.remove(QRegExp("^/")); + } + + qDebug() << "Extracting " + mod.file + " to " + extractToDir; + if(!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) { + // assume error + return false; + } + } + + for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + + qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; + if(!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { + qWarning() << "Failed to extract" << mod.decompFile; + return false; + } + } + + for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + return false; + } + } + return true; +} + +void PackInstallTask::install() +{ + qDebug() << "PackInstallTask::install: " << QThread::currentThreadId(); + setStatus(tr("Installing modpack")); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + // Use a component to add libraries BEFORE Minecraft + if(!createLibrariesComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create libraries component")); + return; + } + + // Minecraft + components->setComponentVersion("net.minecraft", m_version.minecraft, true); + + // Loader + if(m_version.loader.type == QString("forge")) + { + auto version = getVersionForLoader("net.minecraftforge"); + if(version == Q_NULLPTR) return; + + components->setComponentVersion("net.minecraftforge", version, true); + } + else if(m_version.loader.type == QString("fabric")) + { + auto version = getVersionForLoader("net.fabricmc.fabric-loader"); + if(version == Q_NULLPTR) return; + + components->setComponentVersion("net.fabricmc.fabric-loader", version, true); + } + else if(m_version.loader.type != QString()) + { + emitFailed(tr("Unknown loader type: ") + m_version.loader.type); + return; + } + + for(const auto & componentUid : componentsToInstall.keys()) { + auto version = componentsToInstall.value(componentUid); + components->setComponentVersion(componentUid, version->version()); + } + + components->installJarMods(jarmods); + + // Use a component to fill in the rest of the data + // todo: use more detection + if(!createPackComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create pack component")); + return; + } + + components->saveNow(); + + instance.setName(m_instName); + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + jarmods.clear(); + emitSucceeded(); +} + +} diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h new file mode 100644 index 00000000..15fd9b32 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h @@ -0,0 +1,102 @@ +#pragma once + +#include <meta/VersionList.h> +#include "ATLPackManifest.h" + +#include "InstanceTask.h" +#include "multimc_logic_export.h" +#include "net/NetJob.h" +#include "settings/INISettingsObject.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "meta/Version.h" + +#include <nonstd/optional> + +namespace ATLauncher { + +class MULTIMC_LOGIC_EXPORT UserInteractionSupport { + +public: + /** + * Requests a user interaction to select which optional mods should be installed. + */ + virtual QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0; + + /** + * Requests a user interaction to select a component version from a given version list + * and constrained to a given Minecraft version. + */ + virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + +}; + +class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +{ +Q_OBJECT + +public: + explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version); + virtual ~PackInstallTask(){} + + bool canAbort() const override { return true; } + bool abort() override; + +protected: + virtual void executeTask() override; + +private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + + void onModsDownloaded(); + void onModsExtracted(); + +private: + QString getDirForModType(ModType type, QString raw); + QString getVersionForLoader(QString uid); + QString detectLibrary(VersionLibrary library); + + bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile); + bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile); + + void installConfigs(); + void extractConfigs(); + void downloadMods(); + bool extractMods( + const QMap<QString, VersionMod> &toExtract, + const QMap<QString, VersionMod> &toDecomp, + const QMap<QString, QString> &toCopy + ); + void install(); + +private: + UserInteractionSupport *m_support; + + bool abortable = false; + + NetJobPtr jobPtr; + QByteArray response; + + QString m_pack; + QString m_version_name; + PackVersion m_version; + + QMap<QString, VersionMod> modsToExtract; + QMap<QString, VersionMod> modsToDecomp; + QMap<QString, QString> modsToCopy; + + QString archivePath; + QStringList jarmods; + Meta::VersionPtr minecraftVersion; + QMap<QString, Meta::VersionPtr> componentsToInstall; + + QFuture<nonstd::optional<QStringList>> m_extractFuture; + QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + + QFuture<bool> m_modExtractFuture; + QFutureWatcher<bool> m_modExtractFutureWatcher; + +}; + +} diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp new file mode 100644 index 00000000..e25d8346 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp @@ -0,0 +1,218 @@ +#include "ATLPackManifest.h" + +#include "Json.h" + +static ATLauncher::DownloadType parseDownloadType(QString rawType) { + if(rawType == QString("server")) { + return ATLauncher::DownloadType::Server; + } + else if(rawType == QString("browser")) { + return ATLauncher::DownloadType::Browser; + } + else if(rawType == QString("direct")) { + return ATLauncher::DownloadType::Direct; + } + + return ATLauncher::DownloadType::Unknown; +} + +static ATLauncher::ModType parseModType(QString rawType) { + // See https://wiki.atlauncher.com/mod_types + if(rawType == QString("root")) { + return ATLauncher::ModType::Root; + } + else if(rawType == QString("forge")) { + return ATLauncher::ModType::Forge; + } + else if(rawType == QString("jar")) { + return ATLauncher::ModType::Jar; + } + else if(rawType == QString("mods")) { + return ATLauncher::ModType::Mods; + } + else if(rawType == QString("flan")) { + return ATLauncher::ModType::Flan; + } + else if(rawType == QString("dependency") || rawType == QString("depandency")) { + return ATLauncher::ModType::Dependency; + } + else if(rawType == QString("ic2lib")) { + return ATLauncher::ModType::Ic2Lib; + } + else if(rawType == QString("denlib")) { + return ATLauncher::ModType::DenLib; + } + else if(rawType == QString("coremods")) { + return ATLauncher::ModType::Coremods; + } + else if(rawType == QString("mcpc")) { + return ATLauncher::ModType::MCPC; + } + else if(rawType == QString("plugins")) { + return ATLauncher::ModType::Plugins; + } + else if(rawType == QString("extract")) { + return ATLauncher::ModType::Extract; + } + else if(rawType == QString("decomp")) { + return ATLauncher::ModType::Decomp; + } + else if(rawType == QString("texturepack")) { + return ATLauncher::ModType::TexturePack; + } + else if(rawType == QString("resourcepack")) { + return ATLauncher::ModType::ResourcePack; + } + else if(rawType == QString("shaderpack")) { + return ATLauncher::ModType::ShaderPack; + } + else if(rawType == QString("texturepackextract")) { + return ATLauncher::ModType::TexturePackExtract; + } + else if(rawType == QString("resourcepackextract")) { + return ATLauncher::ModType::ResourcePackExtract; + } + else if(rawType == QString("millenaire")) { + return ATLauncher::ModType::Millenaire; + } + + return ATLauncher::ModType::Unknown; +} + +static void loadVersionLoader(ATLauncher::VersionLoader & p, QJsonObject & obj) { + p.type = Json::requireString(obj, "type"); + p.choose = Json::ensureBoolean(obj, QString("choose"), false); + + auto metadata = Json::requireObject(obj, "metadata"); + p.latest = Json::ensureBoolean(metadata, QString("latest"), false); + p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); + + // Minecraft Forge + if (p.type == "forge") { + p.version = Json::ensureString(metadata, "version", ""); + } + + // Fabric Loader + if (p.type == "fabric") { + p.version = Json::ensureString(metadata, "loader", ""); + } +} + +static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) { + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::requireString(obj, "md5"); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.server = Json::ensureString(obj, "server", ""); +} + +static void loadVersionConfigs(ATLauncher::VersionConfigs & p, QJsonObject & obj) { + p.filesize = Json::requireInteger(obj, "filesize"); + p.sha1 = Json::requireString(obj, "sha1"); +} + +static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { + p.name = Json::requireString(obj, "name"); + p.version = Json::requireString(obj, "version"); + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::ensureString(obj, "md5", ""); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.type_raw = Json::requireString(obj, "type"); + p.type = parseModType(p.type_raw); + + // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge" + // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some + // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best + // it can). + if(p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) { + p.type_raw = "forge"; + p.type = ATLauncher::ModType::Forge; + } + + if(obj.contains("extractTo")) { + p.extractTo_raw = Json::requireString(obj, "extractTo"); + p.extractTo = parseModType(p.extractTo_raw); + p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/"); + } + + if(obj.contains("decompType")) { + p.decompType_raw = Json::requireString(obj, "decompType"); + p.decompType = parseModType(p.decompType_raw); + p.decompFile = Json::requireString(obj, "decompFile"); + } + + p.description = Json::ensureString(obj, QString("description"), ""); + p.optional = Json::ensureBoolean(obj, QString("optional"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); + p.selected = Json::ensureBoolean(obj, QString("selected"), false); + p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); + p.library = Json::ensureBoolean(obj, QString("library"), false); + p.group = Json::ensureString(obj, QString("group"), ""); + if(obj.contains("depends")) { + auto dependsArr = Json::requireArray(obj, "depends"); + for (const auto depends : dependsArr) { + p.depends.append(Json::requireString(depends)); + } + } + + p.client = Json::ensureBoolean(obj, QString("client"), false); + + // computed + p.effectively_hidden = p.hidden || p.library; +} + +void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); + v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); + + if(obj.contains("mainClass")) { + auto main = Json::requireObject(obj, "mainClass"); + v.mainClass = Json::ensureString(main, "mainClass", ""); + } + + if(obj.contains("extraArguments")) { + auto arguments = Json::requireObject(obj, "extraArguments"); + v.extraArguments = Json::ensureString(arguments, "arguments", ""); + } + + if(obj.contains("loader")) { + auto loader = Json::requireObject(obj, "loader"); + loadVersionLoader(v.loader, loader); + } + + if(obj.contains("libraries")) { + auto libraries = Json::requireArray(obj, "libraries"); + for (const auto libraryRaw : libraries) + { + auto libraryObj = Json::requireObject(libraryRaw); + ATLauncher::VersionLibrary target; + loadVersionLibrary(target, libraryObj); + v.libraries.append(target); + } + } + + if(obj.contains("mods")) { + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireObject(modRaw); + ATLauncher::VersionMod mod; + loadVersionMod(mod, modObj); + v.mods.append(mod); + } + } + + if(obj.contains("configs")) { + auto configsObj = Json::requireObject(obj, "configs"); + loadVersionConfigs(v.configs, configsObj); + } +} diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.h b/api/logic/modplatform/atlauncher/ATLPackManifest.h new file mode 100644 index 00000000..17821e4c --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.h @@ -0,0 +1,126 @@ +#pragma once + +#include <QString> +#include <QVector> +#include <QJsonObject> +#include <multimc_logic_export.h> + +namespace ATLauncher +{ + +enum class PackType +{ + Public, + Private +}; + +enum class ModType +{ + Root, + Forge, + Jar, + Mods, + Flan, + Dependency, + Ic2Lib, + DenLib, + Coremods, + MCPC, + Plugins, + Extract, + Decomp, + TexturePack, + ResourcePack, + ShaderPack, + TexturePackExtract, + ResourcePackExtract, + Millenaire, + Unknown +}; + +enum class DownloadType +{ + Server, + Browser, + Direct, + Unknown +}; + +struct VersionLoader +{ + QString type; + bool latest; + bool recommended; + bool choose; + + QString version; +}; + +struct VersionLibrary +{ + QString url; + QString file; + QString server; + QString md5; + DownloadType download; + QString download_raw; +}; + +struct VersionMod +{ + QString name; + QString version; + QString url; + QString file; + QString md5; + DownloadType download; + QString download_raw; + ModType type; + QString type_raw; + + ModType extractTo; + QString extractTo_raw; + QString extractFolder; + + ModType decompType; + QString decompType_raw; + QString decompFile; + + QString description; + bool optional; + bool recommended; + bool selected; + bool hidden; + bool library; + QString group; + QVector<QString> depends; + + bool client; + + // computed + bool effectively_hidden; +}; + +struct VersionConfigs +{ + int filesize; + QString sha1; +}; + +struct PackVersion +{ + QString version; + QString minecraft; + bool noConfigs; + QString mainClass; + QString extraArguments; + + VersionLoader loader; + QVector<VersionLibrary> libraries; + QVector<VersionMod> mods; + VersionConfigs configs; +}; + +MULTIMC_LOGIC_EXPORT void loadVersion(PackVersion & v, QJsonObject & obj); + +} diff --git a/api/logic/modplatform/flame/FileResolvingTask.cpp b/api/logic/modplatform/flame/FileResolvingTask.cpp index 24cafcdd..295574f0 100644 --- a/api/logic/modplatform/flame/FileResolvingTask.cpp +++ b/api/logic/modplatform/flame/FileResolvingTask.cpp @@ -1,7 +1,9 @@ #include "FileResolvingTask.h" #include "Json.h" -const char * metabase = "https://cursemeta.dries007.net"; +namespace { + const char * metabase = "https://cursemeta.dries007.net"; +} Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_toProcess(toProcess) @@ -34,70 +36,14 @@ void Flame::FileResolvingTask::netJobFinished() int index = 0; for(auto & bytes: results) { + auto & out = m_toProcess.files[index]; try { - auto doc = Json::requireDocument(bytes); - auto obj = Json::requireObject(doc); - auto & out = m_toProcess.files[index]; - // result code signifies true failure. - if(obj.contains("code")) - { - qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a negative result:"; - qCritical() << bytes; - failed = true; - continue; - } - out.fileName = Json::requireString(obj, "FileNameOnDisk"); - QString rawUrl = Json::requireString(obj, "DownloadURL"); - out.url = QUrl(rawUrl, QUrl::TolerantMode); - if(!out.url.isValid()) - { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } - // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience - // It is also optional - QJsonObject projObj = Json::ensureObject(obj, "_Project", {}); - if(!projObj.isEmpty()) - { - QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower(); - if(strType == "singlefile") - { - out.type = File::Type::SingleFile; - } - else if(strType == "ctoc") - { - out.type = File::Type::Ctoc; - } - else if(strType == "cmod2") - { - out.type = File::Type::Cmod2; - } - else if(strType == "mod") - { - out.type = File::Type::Mod; - } - else if(strType == "folder") - { - out.type = File::Type::Folder; - } - else if(strType == "modpack") - { - out.type = File::Type::Modpack; - } - else - { - qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of unknown file type:" << strType; - out.type = File::Type::Unknown; - failed = true; - continue; - } - out.targetFolder = Json::ensureString(projObj, "Path", "mods"); - } - out.resolved = true; + failed &= (!out.parseFromBytes(bytes)); } catch (const JSONValidationError &e) { - auto & out = m_toProcess.files[index]; + qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); qCritical() << "JSON:"; diff --git a/api/logic/modplatform/flame/FlamePackIndex.cpp b/api/logic/modplatform/flame/FlamePackIndex.cpp new file mode 100644 index 00000000..3d8ea22a --- /dev/null +++ b/api/logic/modplatform/flame/FlamePackIndex.cpp @@ -0,0 +1,92 @@ +#include "FlamePackIndex.h" + +#include "Json.h" + +void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj) +{ + pack.addonId = Json::requireInteger(obj, "id"); + pack.name = Json::requireString(obj, "name"); + pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.description = Json::ensureString(obj, "summary", ""); + + bool thumbnailFound = false; + auto attachments = Json::requireArray(obj, "attachments"); + for(auto attachmentRaw: attachments) { + auto attachmentObj = Json::requireObject(attachmentRaw); + bool isDefault = attachmentObj.value("isDefault").toBool(false); + if(isDefault) { + thumbnailFound = true; + pack.logoName = Json::requireString(attachmentObj, "title"); + pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); + break; + } + } + + if(!thumbnailFound) { + throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); + } + + auto authors = Json::requireArray(obj, "authors"); + for(auto authorIter: authors) { + auto author = Json::requireObject(authorIter); + Flame::ModpackAuthor packAuthor; + packAuthor.name = Json::requireString(author, "name"); + packAuthor.url = Json::requireString(author, "url"); + pack.authors.append(packAuthor); + } + int defaultFileId = Json::requireInteger(obj, "defaultFileId"); + + bool found = false; + // check if there are some files before adding the pack + auto files = Json::requireArray(obj, "latestFiles"); + for(auto fileIter: files) { + auto file = Json::requireObject(fileIter); + int id = Json::requireInteger(file, "id"); + + // NOTE: for now, ignore everything that's not the default... + if(id != defaultFileId) { + continue; + } + + auto versionArray = Json::requireArray(file, "gameVersion"); + if(versionArray.size() < 1) { + continue; + } + + found = true; + break; + } + if(!found) { + throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); + } +} + +void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) +{ + QVector<Flame::IndexedVersion> unsortedVersions; + for(auto versionIter: arr) { + auto version = Json::requireObject(versionIter); + Flame::IndexedVersion file; + + file.addonId = pack.addonId; + file.fileId = Json::requireInteger(version, "id"); + auto versionArray = Json::requireArray(version, "gameVersion"); + if(versionArray.size() < 1) { + continue; + } + + // pick the latest version supported + file.mcVersion = versionArray[0].toString(); + file.version = Json::requireString(version, "displayName"); + file.downloadUrl = Json::requireString(version, "downloadUrl"); + unsortedVersions.append(file); + } + + auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool + { + return a.fileId > b.fileId; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + pack.versions = unsortedVersions; + pack.versionsLoaded = true; +} diff --git a/api/logic/modplatform/flame/FlamePackIndex.h b/api/logic/modplatform/flame/FlamePackIndex.h new file mode 100644 index 00000000..cdeb2c13 --- /dev/null +++ b/api/logic/modplatform/flame/FlamePackIndex.h @@ -0,0 +1,43 @@ +#pragma once + +#include <QList> +#include <QMetaType> +#include <QString> +#include <QVector> + +#include "multimc_logic_export.h" + +namespace Flame { + +struct ModpackAuthor { + QString name; + QString url; +}; + +struct IndexedVersion { + int addonId; + int fileId; + QString version; + QString mcVersion; + QString downloadUrl; +}; + +struct IndexedPack +{ + int addonId; + QString name; + QString description; + QList<ModpackAuthor> authors; + QString logoName; + QString logoUrl; + QString websiteUrl; + + bool versionsLoaded = false; + QVector<IndexedVersion> versions; +}; + +MULTIMC_LOGIC_EXPORT void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +MULTIMC_LOGIC_EXPORT void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); +} + +Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/api/logic/modplatform/flame/PackManifest.cpp b/api/logic/modplatform/flame/PackManifest.cpp index 0f57c9bc..b928fd16 100644 --- a/api/logic/modplatform/flame/PackManifest.cpp +++ b/api/logic/modplatform/flame/PackManifest.cpp @@ -21,7 +21,7 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); - for (const auto & item : arr) + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; @@ -38,7 +38,7 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) m.version = Json::ensureString(manifest, QString("version"), QString()); m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward"); auto arr = Json::ensureArray(manifest, "files", QJsonArray()); - for (const auto & item : arr) + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::File file; @@ -64,3 +64,63 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath) } loadManifestV1(m, obj); } + +bool Flame::File::parseFromBytes(const QByteArray& bytes) +{ + auto doc = Json::requireDocument(bytes); + auto obj = Json::requireObject(doc); + // result code signifies true failure. + if(obj.contains("code")) + { + qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:"; + qCritical() << bytes; + return false; + } + fileName = Json::requireString(obj, "FileNameOnDisk"); + QString rawUrl = Json::requireString(obj, "DownloadURL"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if(!url.isValid()) + { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } + // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience + // It is also optional + QJsonObject projObj = Json::ensureObject(obj, "_Project", {}); + if(!projObj.isEmpty()) + { + QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower(); + if(strType == "singlefile") + { + type = File::Type::SingleFile; + } + else if(strType == "ctoc") + { + type = File::Type::Ctoc; + } + else if(strType == "cmod2") + { + type = File::Type::Cmod2; + } + else if(strType == "mod") + { + type = File::Type::Mod; + } + else if(strType == "folder") + { + type = File::Type::Folder; + } + else if(strType == "modpack") + { + type = File::Type::Modpack; + } + else + { + qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType; + type = File::Type::Unknown; + return false; + } + targetFolder = Json::ensureString(projObj, "Path", "mods"); + } + resolved = true; + return true; +} diff --git a/api/logic/modplatform/flame/PackManifest.h b/api/logic/modplatform/flame/PackManifest.h index 34232eee..02f39f0e 100644 --- a/api/logic/modplatform/flame/PackManifest.h +++ b/api/logic/modplatform/flame/PackManifest.h @@ -8,6 +8,9 @@ namespace Flame { struct File { + // NOTE: throws JSONValidationError + bool parseFromBytes(const QByteArray &bytes); + int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional'. This is at the time of writing unused. diff --git a/api/logic/modplatform/ftb/FtbPackFetchTask.cpp b/api/logic/modplatform/legacy_ftb/PackFetchTask.cpp index 19f6c31a..c2ef6436 100644 --- a/api/logic/modplatform/ftb/FtbPackFetchTask.cpp +++ b/api/logic/modplatform/legacy_ftb/PackFetchTask.cpp @@ -1,32 +1,36 @@ -#include "FtbPackFetchTask.h" +#include "PackFetchTask.h" +#include "PrivatePackManager.h" + #include <QDomDocument> -#include "FtbPrivatePackManager.h" +#include <BuildConfig.h> + +namespace LegacyFTB { -void FtbPackFetchTask::fetch() +void PackFetchTask::fetch() { publicPacks.clear(); thirdPartyPacks.clear(); - NetJob *netJob = new NetJob("FtbModpackFetch"); + NetJob *netJob = new NetJob("LegacyFTB::ModpackFetch"); - QUrl publicPacksUrl = QUrl("https://ftb.cursecdn.com/FTB2/static/modpacks.xml"); + QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); netJob->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData)); - QUrl thirdPartyUrl = QUrl("https://ftb.cursecdn.com/FTB2/static/thirdparty.xml"); + QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); netJob->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData)); - QObject::connect(netJob, &NetJob::succeeded, this, &FtbPackFetchTask::fileDownloadFinished); - QObject::connect(netJob, &NetJob::failed, this, &FtbPackFetchTask::fileDownloadFailed); + QObject::connect(netJob, &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); + QObject::connect(netJob, &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); jobPtr.reset(netJob); netJob->start(); } -void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch) +void PackFetchTask::fetchPrivate(const QStringList & toFetch) { - QString privatePackBaseUrl = QString("https://ftb.cursecdn.com/FTB2/static/%1.xml"); + QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; for (auto &packCode: toFetch) { @@ -36,9 +40,9 @@ void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch) QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { - FtbModpackList packs; - parseAndAddPacks(*data, FtbPackType::Private, packs); - foreach(FtbModpack currentPack, packs) + ModpackList packs; + parseAndAddPacks(*data, PackType::Private, packs); + foreach(Modpack currentPack, packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); @@ -63,18 +67,18 @@ void FtbPackFetchTask::fetchPrivate(const QStringList & toFetch) } } -void FtbPackFetchTask::fileDownloadFinished() +void PackFetchTask::fileDownloadFinished() { jobPtr.reset(); QStringList failedLists; - if(!parseAndAddPacks(publicModpacksXmlFileData, FtbPackType::Public, publicPacks)) + if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks)) { failedLists.append(tr("Public Packs")); } - if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, FtbPackType::ThirdParty, thirdPartyPacks)) + if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) { failedLists.append(tr("Third Party Packs")); } @@ -89,7 +93,7 @@ void FtbPackFetchTask::fileDownloadFinished() } } -bool FtbPackFetchTask::parseAndAddPacks(QByteArray &data, FtbPackType packType, FtbModpackList &list) +bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list) { QDomDocument doc; @@ -110,7 +114,7 @@ bool FtbPackFetchTask::parseAndAddPacks(QByteArray &data, FtbPackType packType, { QDomElement element = nodes.at(i).toElement(); - FtbModpack modpack; + Modpack modpack; modpack.name = element.attribute("name"); modpack.currentVersion = element.attribute("version"); modpack.mcVersion = element.attribute("mcVersion"); @@ -159,8 +163,10 @@ bool FtbPackFetchTask::parseAndAddPacks(QByteArray &data, FtbPackType packType, return true; } -void FtbPackFetchTask::fileDownloadFailed(QString reason) +void PackFetchTask::fileDownloadFailed(QString reason) { - qWarning() << "Fetching FtbPacks failed:" << reason; + qWarning() << "Fetching FTBPacks failed:" << reason; emit failed(reason); } + +} diff --git a/api/logic/modplatform/ftb/FtbPackFetchTask.h b/api/logic/modplatform/legacy_ftb/PackFetchTask.h index f955fe83..4a8469b1 100644 --- a/api/logic/modplatform/ftb/FtbPackFetchTask.h +++ b/api/logic/modplatform/legacy_ftb/PackFetchTask.h @@ -6,13 +6,15 @@ #include <QObject> #include "PackHelpers.h" -class MULTIMC_LOGIC_EXPORT FtbPackFetchTask : public QObject { +namespace LegacyFTB { + +class MULTIMC_LOGIC_EXPORT PackFetchTask : public QObject { Q_OBJECT public: - FtbPackFetchTask() = default; - virtual ~FtbPackFetchTask() = default; + PackFetchTask() = default; + virtual ~PackFetchTask() = default; void fetch(); void fetchPrivate(const QStringList &toFetch); @@ -23,18 +25,20 @@ private: QByteArray publicModpacksXmlFileData; QByteArray thirdPartyModpacksXmlFileData; - bool parseAndAddPacks(QByteArray &data, FtbPackType packType, FtbModpackList &list); - FtbModpackList publicPacks; - FtbModpackList thirdPartyPacks; + bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list); + ModpackList publicPacks; + ModpackList thirdPartyPacks; protected slots: void fileDownloadFinished(); void fileDownloadFailed(QString reason); signals: - void finished(FtbModpackList publicPacks, FtbModpackList thirdPartyPacks); + void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void failed(QString reason); - void privateFileDownloadFinished(FtbModpack modpack); + void privateFileDownloadFinished(Modpack modpack); void privateFileDownloadFailed(QString reason, QString packCode); }; + +} diff --git a/api/logic/modplatform/ftb/PackHelpers.h b/api/logic/modplatform/legacy_ftb/PackHelpers.h index 4306caee..566210d0 100644 --- a/api/logic/modplatform/ftb/PackHelpers.h +++ b/api/logic/modplatform/legacy_ftb/PackHelpers.h @@ -5,15 +5,17 @@ #include <QStringList> #include <QMetaType> +namespace LegacyFTB { + //Header for structs etc... -enum class FtbPackType +enum class PackType { Public, ThirdParty, Private }; -struct FtbModpack +struct Modpack { QString name; QString description; @@ -31,11 +33,13 @@ struct FtbModpack bool bugged = false; bool broken = false; - FtbPackType type; + PackType type; QString packCode; }; -//We need it for the proxy model -Q_DECLARE_METATYPE(FtbModpack) +typedef QList<Modpack> ModpackList; -typedef QList<FtbModpack> FtbModpackList; +} + +//We need it for the proxy model +Q_DECLARE_METATYPE(LegacyFTB::Modpack) diff --git a/api/logic/modplatform/ftb/FtbPackInstallTask.cpp b/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp index 9b7689d9..c77f3250 100644 --- a/api/logic/modplatform/ftb/FtbPackInstallTask.cpp +++ b/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp @@ -1,26 +1,32 @@ -#include "FtbPackInstallTask.h" +#include "PackInstallTask.h" + #include "Env.h" #include "MMCZip.h" -#include "QtConcurrent" + #include "BaseInstance.h" #include "FileSystem.h" #include "settings/INISettingsObject.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "minecraft/GradleSpecifier.h" +#include "BuildConfig.h" + +#include <QtConcurrent> + +namespace LegacyFTB { -FtbPackInstallTask::FtbPackInstallTask(FtbModpack pack, QString version) +PackInstallTask::PackInstallTask(Modpack pack, QString version) { m_pack = pack; m_version = version; } -void FtbPackInstallTask::executeTask() +void PackInstallTask::executeTask() { downloadPack(); } -void FtbPackInstallTask::downloadPack() +void PackInstallTask::downloadPack() { setStatus(tr("Downloading zip for %1").arg(m_pack.name)); @@ -30,45 +36,46 @@ void FtbPackInstallTask::downloadPack() entry->setStale(true); QString url; - if(m_pack.type == FtbPackType::Private) + if(m_pack.type == PackType::Private) { - url = QString("http://ftb.cursecdn.com/FTB2/privatepacks/%1").arg(packoffset); + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset); } else { - url = QString("http://ftb.cursecdn.com/FTB2/modpacks/%1").arg(packoffset); + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset); } job->addNetAction(Net::Download::makeCached(url, entry)); archivePath = entry->getFullPath(); netJobContainer.reset(job); - connect(job, &NetJob::succeeded, this, &FtbPackInstallTask::onDownloadSucceeded); - connect(job, &NetJob::failed, this, &FtbPackInstallTask::onDownloadFailed); - connect(job, &NetJob::progress, this, &FtbPackInstallTask::onDownloadProgress); + connect(job, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + connect(job, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + connect(job, &NetJob::progress, this, &PackInstallTask::onDownloadProgress); job->start(); progress(1, 4); } -void FtbPackInstallTask::onDownloadSucceeded() +void PackInstallTask::onDownloadSucceeded() { abortable = false; unzip(); } -void FtbPackInstallTask::onDownloadFailed(QString reason) +void PackInstallTask::onDownloadFailed(QString reason) { + abortable = false; emitFailed(reason); } -void FtbPackInstallTask::onDownloadProgress(qint64 current, qint64 total) +void PackInstallTask::onDownloadProgress(qint64 current, qint64 total) { abortable = true; progress(current, total * 4); setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10)); } -void FtbPackInstallTask::unzip() +void PackInstallTask::unzip() { progress(2, 4); setStatus(tr("Extracting modpack")); @@ -82,22 +89,22 @@ void FtbPackInstallTask::unzip() } m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); - connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &FtbPackInstallTask::onUnzipFinished); - connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &FtbPackInstallTask::onUnzipCanceled); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); } -void FtbPackInstallTask::onUnzipFinished() +void PackInstallTask::onUnzipFinished() { install(); } -void FtbPackInstallTask::onUnzipCanceled() +void PackInstallTask::onUnzipCanceled() { emitAborted(); } -void FtbPackInstallTask::install() +void PackInstallTask::install() { progress(3, 4); setStatus(tr("Installing modpack")); @@ -114,11 +121,12 @@ void FtbPackInstallTask::install() QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath); + instanceSettings->suspendSave(); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); - auto components = instance.getComponentList(); + auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); @@ -194,7 +202,7 @@ void FtbPackInstallTask::install() emitSucceeded(); } -bool FtbPackInstallTask::abort() +bool PackInstallTask::abort() { if(abortable) { @@ -202,3 +210,5 @@ bool FtbPackInstallTask::abort() } return false; } + +} diff --git a/api/logic/modplatform/ftb/FtbPackInstallTask.h b/api/logic/modplatform/legacy_ftb/PackInstallTask.h index 3319025e..f3515781 100644 --- a/api/logic/modplatform/ftb/FtbPackInstallTask.h +++ b/api/logic/modplatform/legacy_ftb/PackInstallTask.h @@ -6,16 +6,21 @@ #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" -#include "modplatform/ftb/PackHelpers.h" +#include "PackHelpers.h" -class MULTIMC_LOGIC_EXPORT FtbPackInstallTask : public InstanceTask +#include <nonstd/optional> + +namespace LegacyFTB { + +class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask { Q_OBJECT public: - explicit FtbPackInstallTask(FtbModpack pack, QString version); - virtual ~FtbPackInstallTask(){} + explicit PackInstallTask(Modpack pack, QString version); + virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: @@ -38,11 +43,13 @@ private slots: private: /* data */ bool abortable = false; std::unique_ptr<QuaZip> m_packZip; - QFuture<QStringList> m_extractFuture; - QFutureWatcher<QStringList> m_extractFutureWatcher; + QFuture<nonstd::optional<QStringList>> m_extractFuture; + QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; NetJobPtr netJobContainer; QString archivePath; - FtbModpack m_pack; + Modpack m_pack; QString m_version; }; + +} diff --git a/api/logic/modplatform/ftb/FtbPrivatePackManager.cpp b/api/logic/modplatform/legacy_ftb/PrivatePackManager.cpp index c3477cec..501e6003 100644 --- a/api/logic/modplatform/ftb/FtbPrivatePackManager.cpp +++ b/api/logic/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -1,10 +1,12 @@ -#include "FtbPrivatePackManager.h" +#include "PrivatePackManager.h" #include <QDebug> #include "FileSystem.h" -void FtbPrivatePackManager::load() +namespace LegacyFTB { + +void PrivatePackManager::load() { try { @@ -18,7 +20,7 @@ void FtbPrivatePackManager::load() } } -void FtbPrivatePackManager::save() const +void PrivatePackManager::save() const { if(!dirty) { @@ -35,3 +37,5 @@ void FtbPrivatePackManager::save() const qWarning() << "Failed to write third party FTB pack codes to" << m_filename; } } + +} diff --git a/api/logic/modplatform/ftb/FtbPrivatePackManager.h b/api/logic/modplatform/legacy_ftb/PrivatePackManager.h index 388224d6..0232bac7 100644 --- a/api/logic/modplatform/ftb/FtbPrivatePackManager.h +++ b/api/logic/modplatform/legacy_ftb/PrivatePackManager.h @@ -5,10 +5,12 @@ #include <QFile> #include "multimc_logic_export.h" -class MULTIMC_LOGIC_EXPORT FtbPrivatePackManager +namespace LegacyFTB { + +class MULTIMC_LOGIC_EXPORT PrivatePackManager { public: - ~FtbPrivatePackManager() + ~PrivatePackManager() { save(); } @@ -38,3 +40,5 @@ private: QString m_filename = "private_packs.txt"; mutable bool dirty = false; }; + +} diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp new file mode 100644 index 00000000..f22373bc --- /dev/null +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -0,0 +1,209 @@ +#include "FTBPackInstallTask.h" + +#include "BuildConfig.h" +#include "Env.h" +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/ChecksumValidator.h" +#include "settings/INISettingsObject.h" + +namespace ModpacksCH { + +PackInstallTask::PackInstallTask(Modpack pack, QString version) +{ + m_pack = pack; + m_version_name = version; +} + +bool PackInstallTask::abort() +{ + if(abortable) + { + return jobPtr->abort(); + } + return false; +} + +void PackInstallTask::executeTask() +{ + // Find pack version + bool found = false; + VersionInfo version; + + for(auto vInfo : m_pack.versions) { + if (vInfo.name == m_version_name) { + found = true; + version = vInfo; + break; + } + } + + if(!found) { + emitFailed(tr("Failed to find pack version %1").arg(m_version_name)); + return; + } + + auto *netJob = new NetJob("ModpacksCH::VersionFetch"); + 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(); + + QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); +} + +void PackInstallTask::onDownloadSucceeded() +{ + 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 FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + ModpacksCH::Version version; + try + { + ModpacksCH::loadVersion(version, obj); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + m_version = version; + + downloadPack(); +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + jobPtr.reset(); + emitFailed(reason); +} + +void PackInstallTask::downloadPack() +{ + setStatus(tr("Downloading mods...")); + + jobPtr.reset(new NetJob(tr("Mod download"))); + for(auto file : m_version.files) { + if(file.serverOnly) continue; + + QFileInfo fileName(file.name); + auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix(); + + auto entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", cacheName); + entry->setStale(true); + + auto relpath = FS::PathCombine("minecraft", file.path, file.name); + auto path = FS::PathCombine(m_stagingPath, relpath); + + qDebug() << "Will download" << file.url << "to" << path; + filesToCopy[entry->getFullPath()] = path; + + 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); + }); + + jobPtr->start(); +} + +void PackInstallTask::install() +{ + setStatus(tr("Copying modpack files")); + + for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + emitFailed(tr("Failed to copy files")); + return; + } + } + + setStatus(tr("Installing modpack")); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + 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; + + if(target.name == "forge") { + components->setComponentVersion("net.minecraftforge", target.version, true); + } + else if(target.name == "fabric") { + components->setComponentVersion("net.fabricmc.fabric-loader", target.version, true); + } + } + + // install any jar mods + QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods")); + if (jarModsDir.exists()) { + QStringList jarMods; + + for (const auto& info : jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { + jarMods.push_back(info.absoluteFilePath()); + } + + components->installJarMods(jarMods); + } + + components->saveNow(); + + instance.setName(m_instName); + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + emitSucceeded(); +} + +} diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.h b/api/logic/modplatform/modpacksch/FTBPackInstallTask.h new file mode 100644 index 00000000..55db3d3c --- /dev/null +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.h @@ -0,0 +1,47 @@ +#pragma once + +#include "FTBPackManifest.h" + +#include "InstanceTask.h" +#include "multimc_logic_export.h" +#include "net/NetJob.h" + +namespace ModpacksCH { + +class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + explicit PackInstallTask(Modpack pack, QString version); + virtual ~PackInstallTask(){} + + bool canAbort() const override { return true; } + bool abort() override; + +protected: + virtual void executeTask() override; + +private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + +private: + void downloadPack(); + void install(); + +private: + bool abortable = false; + + NetJobPtr jobPtr; + QByteArray response; + + Modpack m_pack; + QString m_version_name; + Version m_version; + + QMap<QString, QString> filesToCopy; + +}; + +} diff --git a/api/logic/modplatform/modpacksch/FTBPackManifest.cpp b/api/logic/modplatform/modpacksch/FTBPackManifest.cpp new file mode 100644 index 00000000..fd99d332 --- /dev/null +++ b/api/logic/modplatform/modpacksch/FTBPackManifest.cpp @@ -0,0 +1,156 @@ +#include "FTBPackManifest.h" + +#include "Json.h" + +static void loadSpecs(ModpacksCH::Specs & s, QJsonObject & obj) +{ + s.id = Json::requireInteger(obj, "id"); + s.minimum = Json::requireInteger(obj, "minimum"); + s.recommended = Json::requireInteger(obj, "recommended"); +} + +static void loadTag(ModpacksCH::Tag & t, QJsonObject & obj) +{ + t.id = Json::requireInteger(obj, "id"); + t.name = Json::requireString(obj, "name"); +} + +static void loadArt(ModpacksCH::Art & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.url = Json::requireString(obj, "url"); + a.type = Json::requireString(obj, "type"); + a.width = Json::requireInteger(obj, "width"); + a.height = Json::requireInteger(obj, "height"); + a.compressed = Json::requireBoolean(obj, "compressed"); + a.sha1 = Json::requireString(obj, "sha1"); + a.size = Json::requireInteger(obj, "size"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadAuthor(ModpacksCH::Author & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.name = Json::requireString(obj, "name"); + a.type = Json::requireString(obj, "type"); + a.website = Json::requireString(obj, "website"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionInfo(ModpacksCH::VersionInfo & v, QJsonObject & obj) +{ + v.id = Json::requireInteger(obj, "id"); + v.name = Json::requireString(obj, "name"); + v.type = Json::requireString(obj, "type"); + v.updated = Json::requireInteger(obj, "updated"); + auto specs = Json::requireObject(obj, "specs"); + loadSpecs(v.specs, specs); +} + +void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.name = Json::requireString(obj, "name"); + m.synopsis = Json::requireString(obj, "synopsis"); + m.description = Json::requireString(obj, "description"); + m.type = Json::requireString(obj, "type"); + m.featured = Json::requireBoolean(obj, "featured"); + m.installs = Json::requireInteger(obj, "installs"); + m.plays = Json::requireInteger(obj, "plays"); + m.updated = Json::requireInteger(obj, "updated"); + m.refreshed = Json::requireInteger(obj, "refreshed"); + auto artArr = Json::requireArray(obj, "art"); + for (QJsonValueRef artRaw : artArr) + { + auto artObj = Json::requireObject(artRaw); + ModpacksCH::Art art; + loadArt(art, artObj); + m.art.append(art); + } + auto authorArr = Json::requireArray(obj, "authors"); + for (QJsonValueRef authorRaw : authorArr) + { + auto authorObj = Json::requireObject(authorRaw); + ModpacksCH::Author author; + loadAuthor(author, authorObj); + m.authors.append(author); + } + auto versionArr = Json::requireArray(obj, "versions"); + for (QJsonValueRef versionRaw : versionArr) + { + auto versionObj = Json::requireObject(versionRaw); + ModpacksCH::VersionInfo version; + loadVersionInfo(version, versionObj); + m.versions.append(version); + } + auto tagArr = Json::requireArray(obj, "tags"); + for (QJsonValueRef tagRaw : tagArr) + { + auto tagObj = Json::requireObject(tagRaw); + ModpacksCH::Tag tag; + loadTag(tag, tagObj); + m.tags.append(tag); + } + m.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionTarget(ModpacksCH::VersionTarget & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.name = Json::requireString(obj, "name"); + a.type = Json::requireString(obj, "type"); + a.version = Json::requireString(obj, "version"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.type = Json::requireString(obj, "type"); + 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.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"); +} + +void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.parent = Json::requireInteger(obj, "parent"); + m.name = Json::requireString(obj, "name"); + m.type = Json::requireString(obj, "type"); + m.installs = Json::requireInteger(obj, "installs"); + m.plays = Json::requireInteger(obj, "plays"); + m.updated = Json::requireInteger(obj, "updated"); + m.refreshed = Json::requireInteger(obj, "refreshed"); + auto specs = Json::requireObject(obj, "specs"); + loadSpecs(m.specs, specs); + auto targetArr = Json::requireArray(obj, "targets"); + for (QJsonValueRef targetRaw : targetArr) + { + auto versionObj = Json::requireObject(targetRaw); + ModpacksCH::VersionTarget target; + loadVersionTarget(target, versionObj); + m.targets.append(target); + } + auto fileArr = Json::requireArray(obj, "files"); + for (QJsonValueRef fileRaw : fileArr) + { + auto fileObj = Json::requireObject(fileRaw); + ModpacksCH::VersionFile file; + loadVersionFile(file, fileObj); + m.files.append(file); + } +} + +//static void loadVersionChangelog(ModpacksCH::VersionChangelog & m, QJsonObject & obj) +//{ +// m.content = Json::requireString(obj, "content"); +// m.updated = Json::requireInteger(obj, "updated"); +//} diff --git a/api/logic/modplatform/modpacksch/FTBPackManifest.h b/api/logic/modplatform/modpacksch/FTBPackManifest.h new file mode 100644 index 00000000..518fffbf --- /dev/null +++ b/api/logic/modplatform/modpacksch/FTBPackManifest.h @@ -0,0 +1,127 @@ +#pragma once + +#include <QString> +#include <QVector> +#include <QUrl> +#include <QJsonObject> +#include <QMetaType> + +#include "multimc_logic_export.h" + +namespace ModpacksCH +{ + +struct Specs +{ + int id; + int minimum; + int recommended; +}; + +struct Tag +{ + int id; + QString name; +}; + +struct Art +{ + int id; + QString url; + QString type; + int width; + int height; + bool compressed; + QString sha1; + int size; + int64_t updated; +}; + +struct Author +{ + int id; + QString name; + QString type; + QString website; + int64_t updated; +}; + +struct VersionInfo +{ + int id; + QString name; + QString type; + int64_t updated; + Specs specs; +}; + +struct Modpack +{ + int id; + QString name; + QString synopsis; + QString description; + QString type; + bool featured; + int installs; + int plays; + int64_t updated; + int64_t refreshed; + QVector<Art> art; + QVector<Author> authors; + QVector<VersionInfo> versions; + QVector<Tag> tags; +}; + +struct VersionTarget +{ + int id; + QString type; + QString name; + QString version; + int64_t updated; +}; + +struct VersionFile +{ + int id; + QString type; + QString path; + QString name; + QString version; + QString url; + QString sha1; + int size; + bool clientOnly; + bool serverOnly; + bool optional; + int64_t updated; +}; + +struct Version +{ + int id; + int parent; + QString name; + QString type; + int installs; + int plays; + int64_t updated; + int64_t refreshed; + Specs specs; + QVector<VersionTarget> targets; + QVector<VersionFile> files; +}; + +struct VersionChangelog +{ + QString content; + int64_t updated; +}; + +MULTIMC_LOGIC_EXPORT void loadModpack(Modpack & m, QJsonObject & obj); + +MULTIMC_LOGIC_EXPORT void loadVersion(Version & m, QJsonObject & obj); +} + +Q_DECLARE_METATYPE(ModpacksCH::Modpack) diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp new file mode 100644 index 00000000..dbce8e53 --- /dev/null +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp @@ -0,0 +1,141 @@ +/* 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 "SingleZipPackInstallTask.h" + +#include "Env.h" +#include "MMCZip.h" +#include "TechnicPackProcessor.h" + +#include <QtConcurrent> +#include <FileSystem.h> + +Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion) +{ + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; +} + +bool Technic::SingleZipPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + +void Technic::SingleZipPackInstallTask::executeTask() +{ + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = ENV.metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob.reset(new NetJob(tr("Modpack download"))); + m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); + connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); + connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SingleZipPackInstallTask::downloadSucceeded() +{ + m_abortable = false; + + setStatus(tr("Extracting modpack")); + QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + m_packZip.reset(new QuaZip(m_archivePath)); + if (!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) +{ + m_abortable = false; + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + m_abortable = true; + setProgress(current / 2, total); +} + +void Technic::SingleZipPackInstallTask::extractFinished() +{ + m_packZip.reset(); + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if (file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if (origPermissions != permissions) + { + if (!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion); +} + +void Technic::SingleZipPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); +} diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.h b/api/logic/modplatform/technic/SingleZipPackInstallTask.h new file mode 100644 index 00000000..ec2ff605 --- /dev/null +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.h @@ -0,0 +1,65 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "InstanceTask.h" +#include "net/NetJob.h" +#include "multimc_logic_export.h" + +#include "quazip.h" + +#include <QFutureWatcher> +#include <QStringList> +#include <QUrl> + +#include <nonstd/optional> + +namespace Technic { + +class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + + bool canAbort() const override { return true; } + bool abort() override; + +protected: + void executeTask() override; + + +private slots: + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + +private: + bool m_abortable = false; + + QUrl m_sourceUrl; + QString m_minecraftVersion; + QString m_archivePath; + NetJobPtr m_filesNetJob; + std::unique_ptr<QuaZip> m_packZip; + QFuture<nonstd::optional<QStringList>> m_extractFuture; + QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; +}; + +} // namespace Technic diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.cpp b/api/logic/modplatform/technic/SolderPackInstallTask.cpp new file mode 100644 index 00000000..1b4186d4 --- /dev/null +++ b/api/logic/modplatform/technic/SolderPackInstallTask.cpp @@ -0,0 +1,207 @@ +/* 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 "SolderPackInstallTask.h" + +#include <FileSystem.h> +#include <Json.h> +#include <QtConcurrentRun> +#include <MMCZip.h> +#include "TechnicPackProcessor.h" + +Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion) +{ + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; +} + +bool Technic::SolderPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + +void Technic::SolderPackInstallTask::executeTask() +{ + setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); + m_filesNetJob.reset(new NetJob(tr("Finding recommended version"))); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded); + connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::versionSucceeded() +{ + try + { + QJsonDocument doc = Json::requireDocument(m_response); + QJsonObject obj = Json::requireObject(doc); + QString version = Json::requireString(obj, "recommended", "__placeholder__"); + m_sourceUrl = m_sourceUrl.toString() + '/' + version; + } + catch (const JSONValidationError &e) + { + emitFailed(e.cause()); + m_filesNetJob.reset(); + return; + } + + setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString())); + m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"))); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); + connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::fileListSucceeded() +{ + setStatus(tr("Downloading modpack:")); + QStringList modUrls; + try + { + QJsonDocument doc = Json::requireDocument(m_response); + QJsonObject obj = Json::requireObject(doc); + QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); + if (!minecraftVersion.isEmpty()) + m_minecraftVersion = minecraftVersion; + QJsonArray mods = Json::requireArray(obj, "mods", "'mods'"); + for (auto mod: mods) + { + QJsonObject modObject = Json::requireObject(mod); + modUrls.append(Json::requireString(modObject, "url", "'url'")); + } + } + catch (const JSONValidationError &e) + { + emitFailed(e.cause()); + m_filesNetJob.reset(); + return; + } + m_filesNetJob.reset(new NetJob(tr("Downloading modpack"))); + int i = 0; + for (auto &modUrl: modUrls) + { + auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); + m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path)); + i++; + } + + m_modCount = modUrls.size(); + + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::downloadSucceeded() +{ + m_abortable = false; + + setStatus(tr("Extracting modpack")); + m_filesNetJob.reset(); + m_extractFuture = QtConcurrent::run([this]() + { + int i = 0; + QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft"); + FS::ensureFolderPathExists(extractDir); + + while (m_modCount > i) + { + auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); + if (!MMCZip::extractDir(path, extractDir)) + { + return false; + } + i++; + } + return true; + }); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SolderPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SolderPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void Technic::SolderPackInstallTask::downloadFailed(QString reason) +{ + m_abortable = false; + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + m_abortable = true; + setProgress(current / 2, total); +} + +void Technic::SolderPackInstallTask::extractFinished() +{ + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if(file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if(origPermissions != permissions) + { + if(!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true); +} + +void Technic::SolderPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); + return; +} + diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.h b/api/logic/modplatform/technic/SolderPackInstallTask.h new file mode 100644 index 00000000..9f0f20a9 --- /dev/null +++ b/api/logic/modplatform/technic/SolderPackInstallTask.h @@ -0,0 +1,60 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <InstanceTask.h> +#include <net/NetJob.h> +#include <tasks/Task.h> + +#include <QUrl> + +namespace Technic +{ + class MULTIMC_LOGIC_EXPORT SolderPackInstallTask : public InstanceTask + { + Q_OBJECT + public: + explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + + bool canAbort() const override { return true; } + bool abort() override; + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + + private slots: + void versionSucceeded(); + void fileListSucceeded(); + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + + private: + bool m_abortable = false; + + NetJobPtr m_filesNetJob; + QUrl m_sourceUrl; + QString m_minecraftVersion; + QByteArray m_response; + QTemporaryDir m_outputDir; + int m_modCount; + QFuture<bool> m_extractFuture; + QFutureWatcher<bool> m_extractFutureWatcher; + }; +} diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.cpp b/api/logic/modplatform/technic/TechnicPackProcessor.cpp new file mode 100644 index 00000000..52979b7c --- /dev/null +++ b/api/logic/modplatform/technic/TechnicPackProcessor.cpp @@ -0,0 +1,208 @@ +/* Copyright 2020-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 "TechnicPackProcessor.h" + +#include <FileSystem.h> +#include <Json.h> +#include <minecraft/MinecraftInstance.h> +#include <minecraft/PackProfile.h> +#include <quazip.h> +#include <quazipdir.h> +#include <quazipfile.h> +#include <settings/INISettingsObject.h> + +#include <memory> + +void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder) +{ + QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); + QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared<INISettingsObject>(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); + + instance.setName(instName); + + if (instIcon != "default") + { + instance.setIconKey(instIcon); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + QByteArray data; + + QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar"); + QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); + QString fmlMinecraftVersion; + if (QFile::exists(modpackJar)) + { + QuaZip zipFile(modpackJar); + if (!zipFile.open(QuaZip::mdUnzip)) + { + emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); + return; + } + QuaZipDir zipFileRoot(&zipFile, "/"); + if (zipFileRoot.exists("/version.json")) + { + if (zipFileRoot.exists("/fmlversion.properties")) + { + zipFile.setCurrentFile("fmlversion.properties"); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"fmlversion.properties\"!")); + return; + } + QByteArray fmlVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(fmlVersionData); + // If not present, this evaluates to a null string + fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); + } + zipFile.setCurrentFile("version.json", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + if (minecraftVersion.isEmpty()) + emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown")); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->installJarMods({modpackJar}); + + // Forge for 1.4.7 and for 1.5.2 require extra libraries. + // Figure out the forge version and add it as a component + // (the code still comes from the jar mod installed above) + if (zipFileRoot.exists("/forgeversion.properties")) + { + zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + // Really shouldn't happen, but error handling shall not be forgotten + emit failed(tr("Unable to open \"forgeversion.properties\"")); + return; + } + QByteArray forgeVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(forgeVersionData); + QString major, minor, revision, build; + major = iniFile["forge.major.number"].toString(); + minor = iniFile["forge.minor.number"].toString(); + revision = iniFile["forge.revision.number"].toString(); + build = iniFile["forge.build.number"].toString(); + + if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty()) + { + emit failed(tr("Invalid \"forgeversion.properties\"!")); + return; + } + + components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build); + } + + components->saveNow(); + emit succeeded(); + return; + } + } + else if (QFile::exists(versionJson)) + { + QFile file(versionJson); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + // This is the "Vanilla" modpack, excluded by the search code + emit failed(tr("Unable to find a \"version.json\"!")); + return; + } + + try + { + QJsonDocument doc = Json::requireDocument(data); + QJsonObject root = Json::requireObject(doc, "version.json"); + QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); + if (minecraftVersion.isEmpty()) + { + if (fmlMinecraftVersion.isEmpty()) + { + emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); + return; + } + minecraftVersion = fmlMinecraftVersion; + } + components->setComponentVersion("net.minecraft", minecraftVersion, true); + for (auto library: Json::ensureArray(root, "libraries", {})) + { + if (!library.isObject()) + { + continue; + } + + auto libraryObject = Json::ensureObject(library, {}, ""); + auto libraryName = Json::ensureString(libraryObject, "name", "", ""); + + if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-')) + { + QString libraryVersion = libraryName.section(':', 2); + if (!libraryVersion.startsWith("1.7.10-")) + { + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); + } + else + { + // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); + } + } + else if (libraryName.startsWith("net.minecraftforge:minecraftforge:")) + { + components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2)); + } + else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) + { + components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2)); + } + } + } + catch (const JSONValidationError &e) + { + emit failed(tr("Could not understand \"version.json\":\n") + e.cause()); + return; + } + + components->saveNow(); + emit succeeded(); +} diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.h b/api/logic/modplatform/technic/TechnicPackProcessor.h new file mode 100644 index 00000000..2ad803b3 --- /dev/null +++ b/api/logic/modplatform/technic/TechnicPackProcessor.h @@ -0,0 +1,35 @@ +/* Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QString> +#include "settings/SettingsObject.h" + +namespace Technic +{ + // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask + class TechnicPackProcessor : public QObject + { + Q_OBJECT + + signals: + void succeeded(); + void failed(QString reason); + + public: + void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false); + }; +} diff --git a/api/logic/mojang/PackageManifest.cpp b/api/logic/mojang/PackageManifest.cpp new file mode 100644 index 00000000..b3dfd7fc --- /dev/null +++ b/api/logic/mojang/PackageManifest.cpp @@ -0,0 +1,427 @@ +#include "PackageManifest.h" +#include <Json.h> +#include <QDir> +#include <QDirIterator> +#include <QCryptographicHash> +#include <QDebug> + +#ifndef Q_OS_WIN32 +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#endif + +namespace mojang_files { + +const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + +int Path::compare(const Path& rhs) const +{ + auto left_cursor = begin(); + auto left_end = end(); + auto right_cursor = rhs.begin(); + auto right_end = rhs.end(); + + while (left_cursor != left_end && right_cursor != right_end) + { + if(*left_cursor < *right_cursor) + { + return -1; + } + else if(*left_cursor > *right_cursor) + { + return 1; + } + left_cursor++; + right_cursor++; + } + + if(left_cursor == left_end) + { + if(right_cursor == right_end) + { + return 0; + } + return -1; + } + return 1; +} + +void Package::addFile(const Path& path, const File& file) { + addFolder(path.parent_path()); + files[path] = file; +} + +void Package::addFolder(Path folder) { + if(!folder.has_parent_path()) { + return; + } + do { + folders.insert(folder); + folder = folder.parent_path(); + } while(folder.has_parent_path()); +} + +void Package::addLink(const Path& path, const Path& target) { + addFolder(path.parent_path()); + symlinks[path] = target; +} + +void Package::addSource(const FileSource& source) { + sources[source.hash] = source; +} + + +namespace { +void fromJson(QJsonDocument & doc, Package & out) { + std::set<Path> seen_paths; + if (!doc.isObject()) + { + throw JSONValidationError("file manifest is not an object"); + } + QJsonObject root = doc.object(); + + auto filesObj = Json::ensureObject(root, "files"); + auto iter = filesObj.begin(); + while (iter != filesObj.end()) + { + Path objectPath = Path(iter.key()); + auto value = iter.value(); + iter++; + if(seen_paths.count(objectPath)) { + throw JSONValidationError("duplicate path inside manifest, the manifest is invalid"); + } + if (!value.isObject()) + { + throw JSONValidationError("file entry inside manifest is not an an object"); + } + seen_paths.insert(objectPath); + + auto fileObject = value.toObject(); + auto type = Json::requireString(fileObject, "type"); + if(type == "directory") { + out.addFolder(objectPath); + continue; + } + else if(type == "file") { + FileSource bestSource; + File file; + file.executable = Json::ensureBoolean(fileObject, QString("executable"), false); + auto downloads = Json::requireObject(fileObject, "downloads"); + for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) { + FileSource source; + + auto downloadObject = Json::requireObject(iter2.value()); + source.hash = Json::requireString(downloadObject, "sha1"); + source.size = Json::requireInteger(downloadObject, "size"); + source.url = Json::requireString(downloadObject, "url"); + + auto compression = iter2.key(); + if(compression == "raw") { + file.hash = source.hash; + file.size = source.size; + source.compression = Compression::Raw; + } + else if (compression == "lzma") { + source.compression = Compression::Lzma; + } + else { + continue; + } + bestSource.upgrade(source); + } + if(bestSource.isBad()) { + throw JSONValidationError("No valid compression method for file " + iter.key()); + } + out.addFile(objectPath, file); + out.addSource(bestSource); + } + else if(type == "link") { + auto target = Json::requireString(fileObject, "target"); + out.symlinks[objectPath] = target; + out.addLink(objectPath, target); + } + else { + throw JSONValidationError("Invalid item type in manifest: " + type); + } + } + // make sure the containing folder exists + out.folders.insert(Path()); +} +} + +Package Package::fromManifestContents(const QByteArray& contents) +{ + Package out; + try + { + auto doc = Json::requireDocument(contents, "Manifest"); + fromJson(doc, out); + return out; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse manifest: %1").arg(e.cause()); + out.valid = false; + return out; + } +} + +Package Package::fromManifestFile(const QString & filename) { + Package out; + try + { + auto doc = Json::requireDocument(filename, filename); + fromJson(doc, out); + return out; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause()); + out.valid = false; + return out; + } +} + +#ifndef Q_OS_WIN32 + +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> + +namespace { +// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves +bool actually_read_symlink_target(const QString & filepath, Path & out) +{ + struct ::stat st; + // FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls. + QByteArray nativePath = filepath.toUtf8(); + const char * filepath_cstr = nativePath.data(); + + if (lstat(filepath_cstr, &st) != 0) + { + return false; + } + + auto size = st.st_size ? st.st_size + 1 : PATH_MAX; + std::string temp(size, '\0'); + // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff + do + { + auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size()); + if(link_length == -1) + { + return false; + } + if(std::string::size_type(link_length) < temp.size()) + { + // buffer was long enough and we managed to read the link target. RETURN here. + temp.resize(link_length); + out = Path(QString::fromUtf8(temp.c_str())); + return true; + } + temp.resize(temp.size() * 2); + } while (true); +} +} +#endif + +// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much? +// FIXME: The error handling is just DEFICIENT +Package Package::fromInspectedFolder(const QString& folderPath) +{ + QDir root(folderPath); + + Package out; + QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories); + while(iterator.hasNext()) { + iterator.next(); + + auto fileInfo = iterator.fileInfo(); + auto relPath = root.relativeFilePath(fileInfo.filePath()); + // FIXME: this is probably completely busted on Windows anyway, so just disable it. + // Qt makes shit up and doesn't understand the platform details + // TODO: Actually use a filesystem library that isn't terrible and has decen license. + // I only know one, and I wrote it. Sadly, currently proprietary. PAIN. +#ifndef Q_OS_WIN32 + if(fileInfo.isSymLink()) { + Path targetPath; + if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) { + qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); + out.valid = false; + } + out.addLink(relPath, targetPath); + } + else +#endif + if(fileInfo.isDir()) { + out.addFolder(relPath); + } + else if(fileInfo.isFile()) { + File f; + f.executable = fileInfo.isExecutable(); + f.size = fileInfo.size(); + // FIXME: async / optimize the hashing + QFile input(fileInfo.absoluteFilePath()); + if(!input.open(QIODevice::ReadOnly)) { + qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath(); + out.valid = false; + break; + } + f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData(); + out.addFile(relPath, f); + } + else { + // Something else... oh my + qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); + out.valid = false; + break; + } + } + out.folders.insert(Path(".")); + out.valid = true; + return out; +} + +namespace { +struct shallow_first_sort +{ + bool operator()(const Path &lhs, const Path &rhs) const + { + auto lhs_depth = lhs.length(); + auto rhs_depth = rhs.length(); + if(lhs_depth < rhs_depth) + { + return true; + } + else if(lhs_depth == rhs_depth) + { + if(lhs < rhs) + { + return true; + } + } + return false; + } +}; + +struct deep_first_sort +{ + bool operator()(const Path &lhs, const Path &rhs) const + { + auto lhs_depth = lhs.length(); + auto rhs_depth = rhs.length(); + if(lhs_depth > rhs_depth) + { + return true; + } + else if(lhs_depth == rhs_depth) + { + if(lhs < rhs) + { + return true; + } + } + return false; + } +}; +} + +UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to) +{ + UpdateOperations out; + + if(!from.valid || !to.valid) { + out.valid = false; + return out; + } + + // Files + for(auto iter = from.files.begin(); iter != from.files.end(); iter++) { + const auto ¤t_hash = iter->second.hash; + const auto ¤t_executable = iter->second.executable; + const auto &path = iter->first; + + auto iter2 = to.files.find(path); + if(iter2 == to.files.end()) { + // removed + out.deletes.push_back(path); + continue; + } + auto new_hash = iter2->second.hash; + auto new_executable = iter2->second.executable; + if (current_hash != new_hash) { + out.deletes.push_back(path); + out.downloads.emplace( + std::pair<Path, FileDownload>{ + path, + FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable) + } + ); + } + else if (current_executable != new_executable) { + out.executable_fixes[path] = new_executable; + } + } + for(auto iter = to.files.begin(); iter != to.files.end(); iter++) { + auto path = iter->first; + if(!from.files.count(path)) { + out.downloads.emplace( + std::pair<Path, FileDownload>{ + path, + FileDownload(to.sources.at(iter->second.hash), iter->second.executable) + } + ); + } + } + + // Folders + std::set<Path, deep_first_sort> remove_folders; + std::set<Path, shallow_first_sort> make_folders; + for(auto from_path: from.folders) { + auto iter = to.folders.find(from_path); + if(iter == to.folders.end()) { + remove_folders.insert(from_path); + } + } + for(auto & rmdir: remove_folders) { + out.rmdirs.push_back(rmdir); + } + for(auto to_path: to.folders) { + auto iter = from.folders.find(to_path); + if(iter == from.folders.end()) { + make_folders.insert(to_path); + } + } + for(auto & mkdir: make_folders) { + out.mkdirs.push_back(mkdir); + } + + // Symlinks + for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) { + const auto ¤t_target = iter->second; + const auto &path = iter->first; + + auto iter2 = to.symlinks.find(path); + if(iter2 == to.symlinks.end()) { + // removed + out.deletes.push_back(path); + continue; + } + const auto &new_target = iter2->second; + if (current_target != new_target) { + out.deletes.push_back(path); + out.mklinks[path] = iter2->second; + } + } + for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) { + auto path = iter->first; + if(!from.symlinks.count(path)) { + out.mklinks[path] = iter->second; + } + } + out.valid = true; + return out; +} + +} diff --git a/api/logic/mojang/PackageManifest.h b/api/logic/mojang/PackageManifest.h new file mode 100644 index 00000000..d01a0554 --- /dev/null +++ b/api/logic/mojang/PackageManifest.h @@ -0,0 +1,173 @@ +#pragma once + +#include <QString> +#include <map> +#include <set> +#include <QStringList> +#include "tasks/Task.h" + +#include "multimc_logic_export.h" + +namespace mojang_files { + +using Hash = QString; +extern const Hash empty_hash; + +// simple-ish path implementation. assumes always relative and does not allow '..' entries +class MULTIMC_LOGIC_EXPORT Path +{ +public: + using parts_type = QStringList; + + Path() = default; + Path(QString string) { + auto parts_in = string.split('/'); + for(auto & part: parts_in) { + if(part.isEmpty() || part == ".") { + continue; + } + if(part == "..") { + if(parts.size()) { + parts.pop_back(); + } + continue; + } + parts.push_back(part); + } + } + + bool has_parent_path() const + { + return parts.size() > 0; + } + + Path parent_path() const + { + if (parts.empty()) + return Path(); + return Path(parts.begin(), std::prev(parts.end())); + } + + bool empty() const + { + return parts.empty(); + } + + int length() const + { + return parts.length(); + } + + bool operator==(const Path & rhs) const { + return parts == rhs.parts; + } + + bool operator!=(const Path & rhs) const { + return parts != rhs.parts; + } + + inline bool operator<(const Path& rhs) const + { + return compare(rhs) < 0; + } + + parts_type::const_iterator begin() const + { + return parts.begin(); + } + + parts_type::const_iterator end() const + { + return parts.end(); + } + + QString toString() const { + return parts.join("/"); + } + +private: + Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) { + auto cursor = start; + while(cursor != end) { + parts.push_back(*cursor); + cursor++; + } + } + int compare(const Path& p) const; + + parts_type parts; +}; + + +enum class Compression { + Raw, + Lzma, + Unknown +}; + + +struct MULTIMC_LOGIC_EXPORT FileSource +{ + Compression compression = Compression::Unknown; + Hash hash; + QString url; + std::size_t size = 0; + void upgrade(const FileSource & other) { + if(compression == Compression::Unknown || other.size < size) { + *this = other; + } + } + bool isBad() const { + return compression == Compression::Unknown; + } +}; + +struct MULTIMC_LOGIC_EXPORT File +{ + Hash hash; + bool executable; + std::uint64_t size = 0; +}; + +struct MULTIMC_LOGIC_EXPORT Package { + static Package fromInspectedFolder(const QString &folderPath); + static Package fromManifestFile(const QString &path); + static Package fromManifestContents(const QByteArray& contents); + + explicit operator bool() const + { + return valid; + } + void addFolder(Path folder); + void addFile(const Path & path, const File & file); + void addLink(const Path & path, const Path & target); + void addSource(const FileSource & source); + + std::map<Hash, FileSource> sources; + bool valid = true; + std::set<Path> folders; + std::map<Path, File> files; + std::map<Path, Path> symlinks; +}; + +struct MULTIMC_LOGIC_EXPORT FileDownload : FileSource +{ + FileDownload(const FileSource& source, bool executable) { + static_cast<FileSource &> (*this) = source; + this->executable = executable; + } + bool executable = false; +}; + +struct MULTIMC_LOGIC_EXPORT UpdateOperations { + static UpdateOperations resolve(const Package & from, const Package & to); + bool valid = false; + std::vector<Path> deletes; + std::vector<Path> rmdirs; + std::vector<Path> mkdirs; + std::map<Path, FileDownload> downloads; + std::map<Path, Path> mklinks; + std::map<Path, bool> executable_fixes; +}; + +} diff --git a/api/logic/mojang/PackageManifest_test.cpp b/api/logic/mojang/PackageManifest_test.cpp new file mode 100644 index 00000000..d4c55c5a --- /dev/null +++ b/api/logic/mojang/PackageManifest_test.cpp @@ -0,0 +1,344 @@ +#include <QTest> +#include <QDebug> +#include "TestUtil.h" + +#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/api/logic/mojang/testdata/1.8.0_202-x64.json b/api/logic/mojang/testdata/1.8.0_202-x64.json new file mode 100644 index 00000000..3d99d719 --- /dev/null +++ b/api/logic/mojang/testdata/1.8.0_202-x64.json @@ -0,0 +1 @@ +{"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/api/logic/mojang/testdata/inspect/a/b.txt b/api/logic/mojang/testdata/inspect/a/b.txt new file mode 100755 index 00000000..e69de29b --- /dev/null +++ b/api/logic/mojang/testdata/inspect/a/b.txt diff --git a/api/logic/mojang/testdata/inspect/a/b/b.txt b/api/logic/mojang/testdata/inspect/a/b/b.txt new file mode 120000 index 00000000..4e19a044 --- /dev/null +++ b/api/logic/mojang/testdata/inspect/a/b/b.txt @@ -0,0 +1 @@ +../b.txt
\ No newline at end of file diff --git a/api/logic/mojang/testdata/inspect_win/a/b.txt b/api/logic/mojang/testdata/inspect_win/a/b.txt new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/api/logic/mojang/testdata/inspect_win/a/b.txt diff --git a/api/logic/mojang/testdata/inspect_win/a/b/b.txt b/api/logic/mojang/testdata/inspect_win/a/b/b.txt new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/api/logic/mojang/testdata/inspect_win/a/b/b.txt diff --git a/api/logic/net/Download.cpp b/api/logic/net/Download.cpp index fc634e3d..3f183b7d 100644 --- a/api/logic/net/Download.cpp +++ b/api/logic/net/Download.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -15,6 +15,7 @@ #include "Download.h" +#include "BuildConfig.h" #include <QFileInfo> #include <QDateTime> #include <QDebug> @@ -94,7 +95,7 @@ void Download::start() return; } - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); QNetworkReply *rep = ENV.qnam().get(request); @@ -167,14 +168,24 @@ bool Download::handleRedirect() } QString redirectStr = QString::fromUtf8(redirectBA); - /* - * IF the URL begins with //, we need to insert the URL scheme. - * See: https://bugreports.qt-project.org/browse/QTBUG-41061 - */ if(redirectStr.startsWith("//")) { + /* + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 + */ redirectStr = m_reply->url().scheme() + ":" + redirectStr; } + else if(redirectStr.startsWith("/")) + { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); + } /* * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. diff --git a/api/logic/net/Download.h b/api/logic/net/Download.h index 9d9d7743..2c436032 100644 --- a/api/logic/net/Download.h +++ b/api/logic/net/Download.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/net/HttpMetaCache.cpp b/api/logic/net/HttpMetaCache.cpp index 66325f71..4bc8fbc8 100644 --- a/api/logic/net/HttpMetaCache.cpp +++ b/api/logic/net/HttpMetaCache.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/net/HttpMetaCache.h b/api/logic/net/HttpMetaCache.h index 6e240723..c3248793 100644 --- a/api/logic/net/HttpMetaCache.h +++ b/api/logic/net/HttpMetaCache.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/net/NetAction.h b/api/logic/net/NetAction.h index e1d2881e..02b249a3 100644 --- a/api/logic/net/NetAction.h +++ b/api/logic/net/NetAction.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/net/NetJob.cpp b/api/logic/net/NetJob.cpp index 19ef742e..029d9e34 100644 --- a/api/logic/net/NetJob.cpp +++ b/api/logic/net/NetJob.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -214,3 +214,5 @@ bool NetJob::addNetAction(NetActionPtr action) } return true; } + +NetJob::~NetJob() = default; diff --git a/api/logic/net/NetJob.h b/api/logic/net/NetJob.h index 3051b943..480d8037 100644 --- a/api/logic/net/NetJob.h +++ b/api/logic/net/NetJob.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -34,7 +34,7 @@ public: { setObjectName(job_name); } - virtual ~NetJob() {} + virtual ~NetJob(); bool addNetAction(NetActionPtr action); diff --git a/api/logic/net/PasteUpload.cpp b/api/logic/net/PasteUpload.cpp index 3526e207..cb470c49 100644 --- a/api/logic/net/PasteUpload.cpp +++ b/api/logic/net/PasteUpload.cpp @@ -5,6 +5,7 @@ #include <QJsonArray> #include <QJsonDocument> #include <QFile> +#include <BuildConfig.h> PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window) { @@ -34,7 +35,7 @@ bool PasteUpload::validateText() void PasteUpload::executeTask() { QNetworkRequest request(QUrl("https://api.paste.ee/v1/pastes")); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setRawHeader("Content-Type", "application/json"); request.setRawHeader("Content-Length", QByteArray::number(m_jsonContent.size())); diff --git a/api/logic/net/URLConstants.cpp b/api/logic/net/URLConstants.cpp deleted file mode 100644 index 5d848d80..00000000 --- a/api/logic/net/URLConstants.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "URLConstants.h" - -namespace URLConstants { - -QString getLegacyJarUrl(QString version) -{ - return "http://" + AWS_DOWNLOAD_VERSIONS + getJarPath(version); -} - -QString getJarPath(QString version) -{ - return version + "/" + version + ".jar"; -} - - -} diff --git a/api/logic/net/URLConstants.h b/api/logic/net/URLConstants.h deleted file mode 100644 index 22d128b2..00000000 --- a/api/logic/net/URLConstants.h +++ /dev/null @@ -1,40 +0,0 @@ -/* Copyright 2013-2018 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <QString> - -namespace URLConstants -{ -const QString AWS_DOWNLOAD_VERSIONS("s3.amazonaws.com/Minecraft.Download/versions/"); -const QString RESOURCE_BASE("resources.download.minecraft.net/"); -const QString LIBRARY_BASE("libraries.minecraft.net/"); -//const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/"); -const QString SKINS_BASE("crafatar.com/skins/"); -const QString AUTH_BASE("authserver.mojang.com/"); -const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json"); -const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json"); -const QString MOJANG_STATUS_URL("http://status.mojang.com/check"); -const QString MOJANG_STATUS_NEWS_URL("http://status.mojang.com/news"); -const QString LITELOADER_URL("http://dl.liteloader.com/versions/versions.json"); -const QString IMGUR_BASE_URL("https://api.imgur.com/3/"); -const QString FMLLIBS_OUR_BASE_URL("http://files.multimc.org/fmllibs/"); -const QString FMLLIBS_FORGE_BASE_URL("http://files.minecraftforge.net/fmllibs/"); -const QString TRANSLATIONS_BASE_URL("http://files.multimc.org/translations/"); - -QString getJarPath(QString version); -QString getLegacyJarUrl(QString version); -} diff --git a/api/logic/news/NewsChecker.cpp b/api/logic/news/NewsChecker.cpp index 1f5058ef..c66f49e1 100644 --- a/api/logic/news/NewsChecker.cpp +++ b/api/logic/news/NewsChecker.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/news/NewsChecker.h b/api/logic/news/NewsChecker.h index 7af5bd5d..c473ecab 100644 --- a/api/logic/news/NewsChecker.h +++ b/api/logic/news/NewsChecker.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/news/NewsEntry.cpp b/api/logic/news/NewsEntry.cpp index 82d654be..7eff657b 100644 --- a/api/logic/news/NewsEntry.cpp +++ b/api/logic/news/NewsEntry.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/news/NewsEntry.h b/api/logic/news/NewsEntry.h index 832766fb..0dbc70a5 100644 --- a/api/logic/news/NewsEntry.h +++ b/api/logic/news/NewsEntry.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/screenshots/ImgurAlbumCreation.cpp b/api/logic/screenshots/ImgurAlbumCreation.cpp index 3d32f597..1f195f00 100644 --- a/api/logic/screenshots/ImgurAlbumCreation.cpp +++ b/api/logic/screenshots/ImgurAlbumCreation.cpp @@ -6,13 +6,13 @@ #include <QUrl> #include <QStringList> -#include "net/URLConstants.h" +#include "BuildConfig.h" #include "Env.h" #include <QDebug> ImgurAlbumCreation::ImgurAlbumCreation(QList<ScreenshotPtr> screenshots) : NetAction(), m_screenshots(screenshots) { - m_url = URLConstants::IMGUR_BASE_URL + "album.json"; + m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; m_status = Job_NotStarted; } @@ -20,9 +20,9 @@ void ImgurAlbumCreation::start() { m_status = Job_InProgress; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3"); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QStringList hashes; diff --git a/api/logic/screenshots/ImgurUpload.cpp b/api/logic/screenshots/ImgurUpload.cpp index 74165869..7e95d5ca 100644 --- a/api/logic/screenshots/ImgurUpload.cpp +++ b/api/logic/screenshots/ImgurUpload.cpp @@ -8,13 +8,13 @@ #include <QFile> #include <QUrl> -#include "net/URLConstants.h" +#include "BuildConfig.h" #include "Env.h" #include <QDebug> ImgurUpload::ImgurUpload(ScreenshotPtr shot) : NetAction(), m_shot(shot) { - m_url = URLConstants::IMGUR_BASE_URL + "upload.json"; + m_url = BuildConfig.IMGUR_BASE_URL + "upload.json"; m_status = Job_NotStarted; } @@ -23,8 +23,8 @@ void ImgurUpload::start() finished = false; m_status = Job_InProgress; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); - request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QFile f(m_shot->m_file.absoluteFilePath()); diff --git a/api/logic/settings/INIFile.cpp b/api/logic/settings/INIFile.cpp index 5431443e..6a3c801d 100644 --- a/api/logic/settings/INIFile.cpp +++ b/api/logic/settings/INIFile.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -36,8 +36,10 @@ QString INIFile::unescape(QString orig) { if(c == 'n') out += '\n'; - else if (c == 't') + else if(c == 't') out += '\t'; + else if(c == '#') + out += '#'; else out += c; prev = 0; @@ -67,6 +69,8 @@ QString INIFile::escape(QString orig) out += "\\t"; else if(c == '\\') out += "\\\\"; + else if(c == '#') + out += "\\#"; else out += c; } @@ -120,7 +124,15 @@ bool INIFile::loadFile(QByteArray file) { QString &lineRaw = lines[i]; // Ignore comments. - QString line = lineRaw.left(lineRaw.indexOf('#')).trimmed(); + int commentIndex = 0; + QString line = lineRaw; + // Search for comments until no more escaped # are available + while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) { + if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') { + continue; + } + line = line.left(lineRaw.indexOf('#')).trimmed(); + } int eqPos = line.indexOf('='); if (eqPos == -1) diff --git a/api/logic/settings/INIFile.h b/api/logic/settings/INIFile.h index bdf7fd9a..9e8c68ea 100644 --- a/api/logic/settings/INIFile.h +++ b/api/logic/settings/INIFile.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/INIFile_test.cpp b/api/logic/settings/INIFile_test.cpp index 45f70973..08c2155e 100644 --- a/api/logic/settings/INIFile_test.cpp +++ b/api/logic/settings/INIFile_test.cpp @@ -26,6 +26,7 @@ slots: 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() { @@ -40,7 +41,7 @@ slots: void test_SaveLoad() { QString a = "a"; - QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\"; + QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment"; QString filename = "test_SaveLoad.ini"; // save diff --git a/api/logic/settings/INISettingsObject.cpp b/api/logic/settings/INISettingsObject.cpp index 43791c24..12513403 100644 --- a/api/logic/settings/INISettingsObject.cpp +++ b/api/logic/settings/INISettingsObject.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/INISettingsObject.h b/api/logic/settings/INISettingsObject.h index 3fc09593..313f1512 100644 --- a/api/logic/settings/INISettingsObject.h +++ b/api/logic/settings/INISettingsObject.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/OverrideSetting.cpp b/api/logic/settings/OverrideSetting.cpp index e0e407bd..4396a381 100644 --- a/api/logic/settings/OverrideSetting.cpp +++ b/api/logic/settings/OverrideSetting.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/OverrideSetting.h b/api/logic/settings/OverrideSetting.h index db28e2fc..9f0c98b5 100644 --- a/api/logic/settings/OverrideSetting.h +++ b/api/logic/settings/OverrideSetting.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/PassthroughSetting.cpp b/api/logic/settings/PassthroughSetting.cpp index 58522385..8f93b251 100644 --- a/api/logic/settings/PassthroughSetting.cpp +++ b/api/logic/settings/PassthroughSetting.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/PassthroughSetting.h b/api/logic/settings/PassthroughSetting.h index 7999be97..22008f83 100644 --- a/api/logic/settings/PassthroughSetting.h +++ b/api/logic/settings/PassthroughSetting.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/Setting.cpp b/api/logic/settings/Setting.cpp index 8bb42f5e..cfe5a7f9 100644 --- a/api/logic/settings/Setting.cpp +++ b/api/logic/settings/Setting.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/Setting.h b/api/logic/settings/Setting.h index 0e9224c5..a31193ac 100644 --- a/api/logic/settings/Setting.h +++ b/api/logic/settings/Setting.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/SettingsObject.cpp b/api/logic/settings/SettingsObject.cpp index d1851415..8a0bc045 100644 --- a/api/logic/settings/SettingsObject.cpp +++ b/api/logic/settings/SettingsObject.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/settings/SettingsObject.h b/api/logic/settings/SettingsObject.h index 10a34f81..3ebdebdf 100644 --- a/api/logic/settings/SettingsObject.h +++ b/api/logic/settings/SettingsObject.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/status/StatusChecker.cpp b/api/logic/status/StatusChecker.cpp index ffadcc74..38fc2163 100644 --- a/api/logic/status/StatusChecker.cpp +++ b/api/logic/status/StatusChecker.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -15,12 +15,12 @@ #include "StatusChecker.h" -#include <net/URLConstants.h> - #include <QByteArray> #include <QDebug> +#include <BuildConfig.h> + StatusChecker::StatusChecker() { @@ -43,7 +43,7 @@ void StatusChecker::reloadStatus() // qDebug() << "Reloading status."; NetJob* job = new NetJob("Status JSON"); - job->addNetAction(Net::Download::makeByteArray(URLConstants::MOJANG_STATUS_URL, &dataSink)); + job->addNetAction(Net::Download::makeByteArray(BuildConfig.MOJANG_STATUS_URL, &dataSink)); QObject::connect(job, &NetJob::succeeded, this, &StatusChecker::statusDownloadFinished); QObject::connect(job, &NetJob::failed, this, &StatusChecker::statusDownloadFailed); m_statusNetJob.reset(job); diff --git a/api/logic/status/StatusChecker.h b/api/logic/status/StatusChecker.h index 51a86910..e9961aff 100644 --- a/api/logic/status/StatusChecker.h +++ b/api/logic/status/StatusChecker.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/tasks/Task.cpp b/api/logic/tasks/Task.cpp index f7f2d491..d0ac7569 100644 --- a/api/logic/tasks/Task.cpp +++ b/api/logic/tasks/Task.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -39,23 +39,49 @@ void Task::setProgress(qint64 current, qint64 total) void Task::start() { - m_running = true; + switch(m_state) + { + case State::Inactive: + { + qDebug() << "Task" << describe() << "starting for the first time"; + break; + } + case State::AbortedByUser: + { + qDebug() << "Task" << describe() << "restarting for after being aborted by user"; + break; + } + case State::Failed: + { + qDebug() << "Task" << describe() << "restarting for after failing at first"; + break; + } + case State::Succeeded: + { + qDebug() << "Task" << describe() << "restarting for after succeeding at first"; + break; + } + case State::Running: + { + qWarning() << "MultiMC tried to start task" << describe() << "while it was already running!"; + return; + } + } + // NOTE: only fall thorugh to here in end states + m_state = State::Running; emit started(); - qDebug() << "Task" << describe() << "started"; executeTask(); } void Task::emitFailed(QString reason) { // Don't fail twice. - if (!m_running) + if (!isRunning()) { qCritical() << "Task" << describe() << "failed while not running!!!!: " << reason; return; } - m_running = false; - m_finished = true; - m_succeeded = false; + m_state = State::Failed; m_failReason = reason; qCritical() << "Task" << describe() << "failed: " << reason; emit failed(reason); @@ -65,14 +91,12 @@ void Task::emitFailed(QString reason) void Task::emitAborted() { // Don't abort twice. - if (!m_running) + if (!isRunning()) { qCritical() << "Task" << describe() << "aborted while not running!!!!"; return; } - m_running = false; - m_finished = true; - m_succeeded = false; + m_state = State::AbortedByUser; m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; emit failed(m_failReason); @@ -82,14 +106,12 @@ void Task::emitAborted() void Task::emitSucceeded() { // Don't succeed twice. - if (!m_running) + if (!isRunning()) { qCritical() << "Task" << describe() << "succeeded while not running!!!!"; return; } - m_running = false; - m_finished = true; - m_succeeded = true; + m_state = State::Succeeded; qDebug() << "Task" << describe() << "succeeded"; emit succeeded(); emit finished(); @@ -116,17 +138,17 @@ QString Task::describe() bool Task::isRunning() const { - return m_running; + return m_state == State::Running; } bool Task::isFinished() const { - return m_finished; + return m_state != State::Running && m_state != State::Inactive; } bool Task::wasSuccessful() const { - return m_succeeded; + return m_state == State::Succeeded; } QString Task::failReason() const diff --git a/api/logic/tasks/Task.h b/api/logic/tasks/Task.h index 58dbc4ca..7ed7086c 100644 --- a/api/logic/tasks/Task.h +++ b/api/logic/tasks/Task.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -25,6 +25,16 @@ class MULTIMC_LOGIC_EXPORT Task : public QObject { Q_OBJECT public: + enum class State + { + Inactive, + Running, + Succeeded, + Failed, + AbortedByUser + }; + +public: explicit Task(QObject *parent = 0); virtual ~Task() {}; @@ -88,9 +98,7 @@ public slots: void setProgress(qint64 current, qint64 total); private: - bool m_running = false; - bool m_finished = false; - bool m_succeeded = false; + State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; QString m_status; diff --git a/api/logic/tools/BaseProfiler.cpp b/api/logic/tools/BaseProfiler.cpp index c7d83549..300d1a73 100644 --- a/api/logic/tools/BaseProfiler.cpp +++ b/api/logic/tools/BaseProfiler.cpp @@ -1,4 +1,5 @@ #include "BaseProfiler.h" +#include "QObjectPtr.h" #include <QProcess> @@ -7,7 +8,7 @@ BaseProfiler::BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QOb { } -void BaseProfiler::beginProfiling(std::shared_ptr<LaunchTask> process) +void BaseProfiler::beginProfiling(shared_qobject_ptr<LaunchTask> process) { beginProfilingImpl(process); } diff --git a/api/logic/tools/BaseProfiler.h b/api/logic/tools/BaseProfiler.h index f3e1ce3d..da817f52 100644 --- a/api/logic/tools/BaseProfiler.h +++ b/api/logic/tools/BaseProfiler.h @@ -1,6 +1,7 @@ #pragma once #include "BaseExternalTool.h" +#include "QObjectPtr.h" #include "multimc_logic_export.h" @@ -17,13 +18,13 @@ public: public slots: - void beginProfiling(std::shared_ptr<LaunchTask> process); + void beginProfiling(shared_qobject_ptr<LaunchTask> process); void abortProfiling(); protected: QProcess *m_profilerProcess; - virtual void beginProfilingImpl(std::shared_ptr<LaunchTask> process) = 0; + virtual void beginProfilingImpl(shared_qobject_ptr<LaunchTask> process) = 0; virtual void abortProfilingImpl(); signals: diff --git a/api/logic/tools/JProfiler.cpp b/api/logic/tools/JProfiler.cpp index b50322fe..1dc0d109 100644 --- a/api/logic/tools/JProfiler.cpp +++ b/api/logic/tools/JProfiler.cpp @@ -17,7 +17,7 @@ private slots: void profilerFinished(int exit, QProcess::ExitStatus status); protected: - void beginProfilingImpl(std::shared_ptr<LaunchTask> process); + void beginProfilingImpl(shared_qobject_ptr<LaunchTask> process); private: int listeningPort = 0; @@ -47,7 +47,7 @@ void JProfiler::profilerFinished(int exit, QProcess::ExitStatus status) } } -void JProfiler::beginProfilingImpl(std::shared_ptr<LaunchTask> process) +void JProfiler::beginProfilingImpl(shared_qobject_ptr<LaunchTask> process) { listeningPort = globalSettings->get("JProfilerPort").toInt(); QProcess *profiler = new QProcess(this); diff --git a/api/logic/tools/JVisualVM.cpp b/api/logic/tools/JVisualVM.cpp index 10886857..b1acc3c0 100644 --- a/api/logic/tools/JVisualVM.cpp +++ b/api/logic/tools/JVisualVM.cpp @@ -18,7 +18,7 @@ private slots: void profilerFinished(int exit, QProcess::ExitStatus status); protected: - void beginProfilingImpl(std::shared_ptr<LaunchTask> process); + void beginProfilingImpl(shared_qobject_ptr<LaunchTask> process); }; @@ -45,7 +45,7 @@ void JVisualVM::profilerFinished(int exit, QProcess::ExitStatus status) } } -void JVisualVM::beginProfilingImpl(std::shared_ptr<LaunchTask> process) +void JVisualVM::beginProfilingImpl(shared_qobject_ptr<LaunchTask> process) { QProcess *profiler = new QProcess(this); QStringList profilerArgs = diff --git a/api/logic/translations/POTranslator.cpp b/api/logic/translations/POTranslator.cpp new file mode 100644 index 00000000..1ffcb9a4 --- /dev/null +++ b/api/logic/translations/POTranslator.cpp @@ -0,0 +1,373 @@ +#include "POTranslator.h" + +#include <QDebug> +#include "FileSystem.h" + +struct POEntry +{ + QString text; + bool fuzzy; +}; + +struct POTranslatorPrivate +{ + QString filename; + QHash<QByteArray, POEntry> mapping; + QHash<QByteArray, POEntry> mapping_disambiguatrion; + bool loaded = false; + + void reload(); +}; + +class ParserArray : public QByteArray +{ +public: + ParserArray(const QByteArray &in) : QByteArray(in) + { + } + bool chomp(const char * data, int length) + { + if(startsWith(data)) + { + remove(0, length); + return true; + } + return false; + } + bool chompString(QByteArray & appendHere) + { + QByteArray msg; + bool escape = false; + if(size() < 2) + { + qDebug() << "String fragment is too short"; + return false; + } + if(!startsWith('"')) + { + qDebug() << "String fragment does not start with \""; + return false; + } + if(!endsWith('"')) + { + qDebug() << "String fragment does not end with \", instead, there is" << at(size() - 1); + return false; + } + for(int i = 1; i < size() - 1; i++) + { + char c = operator[](i); + if(escape) + { + switch(c) + { + case 'r': + msg += '\r'; + break; + case 'n': + msg += '\n'; + break; + case 't': + msg += '\t'; + break; + case 'v': + msg += '\v'; + break; + case 'a': + msg += '\a'; + break; + case 'b': + msg += '\b'; + break; + case 'f': + msg += '\f'; + break; + case '"': + msg += '"'; + break; + case '\\': + msg.append('\\'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + { + int octal_start = i; + while ((c = operator[](i)) >= '0' && c <= '7') + { + i++; + if (i == length() - 1) + { + qDebug() << "Something went bad while parsing an octal escape string..."; + return false; + } + } + msg += mid(octal_start, i - octal_start).toUInt(0, 8); + break; + } + case 'x': + { + // chomp the 'x' + i++; + int hex_start = i; + while (isxdigit(operator[](i))) + { + i++; + if (i == length() - 1) + { + qDebug() << "Something went bad while parsing a hex escape string..."; + return false; + } + } + msg += mid(hex_start, i - hex_start).toUInt(0, 16); + break; + } + default: + { + qDebug() << "Invalid escape sequence character:" << c; + return false; + } + } + escape = false; + } + else if(c == '\\') + { + escape = true; + } + else + { + msg += c; + } + } + if(escape) + { + qDebug() << "Unterminated escape sequence..."; + return false; + } + appendHere += msg; + return true; + } +}; + +void POTranslatorPrivate::reload() +{ + QFile file(filename); + if(!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text)) + { + qDebug() << "Failed to open PO file:" << filename; + return; + } + + QByteArray context; + QByteArray disambiguation; + QByteArray id; + QByteArray str; + bool fuzzy = false; + bool nextFuzzy = false; + + enum class Mode + { + First, + MessageContext, + MessageId, + MessageString + } mode = Mode::First; + + int lineNumber = 0; + QHash<QByteArray, POEntry> newMapping; + QHash<QByteArray, POEntry> newMapping_disambiguation; + auto endEntry = [&]() { + auto strStr = QString::fromUtf8(str); + // NOTE: PO header has empty id. We skip it. + if(!id.isEmpty()) + { + auto normalKey = context + "|" + id; + newMapping.insert(normalKey, {strStr, fuzzy}); + if(!disambiguation.isEmpty()) + { + auto disambiguationKey = context + "|" + id + "@" + disambiguation; + newMapping_disambiguation.insert(disambiguationKey, {strStr, fuzzy}); + } + } + context.clear(); + disambiguation.clear(); + id.clear(); + str.clear(); + fuzzy = nextFuzzy; + nextFuzzy = false; + }; + while (!file.atEnd()) + { + ParserArray line = file.readLine(); + if(line.endsWith('\n')) + { + line.resize(line.size() - 1); + } + if(line.endsWith('\r')) + { + line.resize(line.size() - 1); + } + + if(!line.size()) + { + // NIL + } + else if(line[0] == '#') + { + if(line.contains(", fuzzy")) + { + nextFuzzy = true; + } + } + else if(line.startsWith('"')) + { + QByteArray temp; + QByteArray *out = &temp; + + switch(mode) + { + case Mode::First: + qDebug() << "Unexpected escaped string during initial state... line:" << lineNumber; + return; + case Mode::MessageString: + out = &str; + break; + case Mode::MessageContext: + out = &context; + break; + case Mode::MessageId: + out = &id; + break; + } + if(!line.chompString(*out)) + { + qDebug() << "Badly formatted string on line:" << lineNumber; + return; + } + } + else if(line.chomp("msgctxt ", 8)) + { + switch(mode) + { + case Mode::First: + break; + case Mode::MessageString: + endEntry(); + break; + case Mode::MessageContext: + case Mode::MessageId: + qDebug() << "Unexpected msgctxt line:" << lineNumber; + return; + } + if(line.chompString(context)) + { + auto parts = context.split('|'); + context = parts[0]; + if(parts.size() > 1 && !parts[1].isEmpty()) + { + disambiguation = parts[1]; + } + mode = Mode::MessageContext; + } + } + else if (line.chomp("msgid ", 6)) + { + switch(mode) + { + case Mode::MessageContext: + case Mode::First: + break; + case Mode::MessageString: + endEntry(); + break; + case Mode::MessageId: + qDebug() << "Unexpected msgid line:" << lineNumber; + return; + } + if(line.chompString(id)) + { + mode = Mode::MessageId; + } + } + else if (line.chomp("msgstr ", 7)) + { + switch(mode) + { + case Mode::First: + case Mode::MessageString: + case Mode::MessageContext: + qDebug() << "Unexpected msgstr line:" << lineNumber; + return; + case Mode::MessageId: + break; + } + if(line.chompString(str)) + { + mode = Mode::MessageString; + } + } + else + { + qDebug() << "I did not understand line: " << lineNumber << ":" << QString::fromUtf8(line); + } + lineNumber++; + } + endEntry(); + mapping = std::move(newMapping); + mapping_disambiguatrion = std::move(newMapping_disambiguation); + loaded = true; +} + +POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslator(parent) +{ + d = new POTranslatorPrivate; + d->filename = filename; + d->reload(); +} + +QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, int n) const +{ + if(disambiguation) + { + auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation); + auto iter = d->mapping_disambiguatrion.find(disambiguationKey); + if(iter != d->mapping_disambiguatrion.end()) + { + auto & entry = *iter; + if(entry.text.isEmpty()) + { + qDebug() << "Translation entry has no content:" << disambiguationKey; + } + if(entry.fuzzy) + { + qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text; + } + return entry.text; + } + } + auto key = QByteArray(context) + "|" + QByteArray(sourceText); + auto iter = d->mapping.find(key); + if(iter != d->mapping.end()) + { + auto & entry = *iter; + if(entry.text.isEmpty()) + { + qDebug() << "Translation entry has no content:" << key; + } + if(entry.fuzzy) + { + qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text; + } + return entry.text; + } + return QString(); +} + +bool POTranslator::isEmpty() const +{ + return !d->loaded; +} diff --git a/api/logic/translations/POTranslator.h b/api/logic/translations/POTranslator.h new file mode 100644 index 00000000..6d518560 --- /dev/null +++ b/api/logic/translations/POTranslator.h @@ -0,0 +1,16 @@ +#pragma once + +#include <QTranslator> + +struct POTranslatorPrivate; + +class POTranslator : public QTranslator +{ + Q_OBJECT +public: + explicit POTranslator(const QString& filename, QObject * parent = nullptr); + QString translate(const char * context, const char * sourceText, const char * disambiguation, int n) const override; + bool isEmpty() const override; +private: + POTranslatorPrivate * d; +}; diff --git a/api/logic/translations/TranslationsModel.cpp b/api/logic/translations/TranslationsModel.cpp index 185cc8c4..29a952b0 100644 --- a/api/logic/translations/TranslationsModel.cpp +++ b/api/logic/translations/TranslationsModel.cpp @@ -8,24 +8,113 @@ #include <QDebug> #include <FileSystem.h> #include <net/NetJob.h> +#include <net/ChecksumValidator.h> #include <Env.h> -#include <net/URLConstants.h> +#include <BuildConfig.h> +#include "Json.h" -const static QLatin1Literal defaultLangCode("en"); +#include "POTranslator.h" + +const static QLatin1Literal defaultLangCode("en_US"); + +enum class FileType +{ + NONE, + QM, + PO +}; struct Language { + Language() + { + updated = true; + } + Language(const QString & _key) + { + key = _key; + locale = QLocale(key); + updated = (key == defaultLangCode); + } + + float percentTranslated() const + { + if (total == 0) + { + return 100.0f; + } + return 100.0f * float(translated) / float(total); + } + + void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy) + { + translated = _translated; + untranslated = _untranslated; + fuzzy = _fuzzy; + total = translated + untranslated + fuzzy; + } + + bool isOfSameNameAs(const Language& other) const + { + return key == other.key; + } + + bool isIdenticalTo(const Language& other) const + { + return + ( + key == other.key && + file_name == other.file_name && + file_size == other.file_size && + file_sha1 == other.file_sha1 && + translated == other.translated && + fuzzy == other.fuzzy && + total == other.fuzzy && + localFileType == other.localFileType + ); + } + + Language & apply(Language & other) + { + if(!isOfSameNameAs(other)) + { + return *this; + } + file_name = other.file_name; + file_size = other.file_size; + file_sha1 = other.file_sha1; + translated = other.translated; + fuzzy = other.fuzzy; + total = other.total; + localFileType = other.localFileType; + return *this; + } + QString key; QLocale locale; bool updated; + + QString file_name = QString(); + std::size_t file_size = 0; + QString file_sha1 = QString(); + + unsigned translated = 0; + unsigned untranslated = 0; + unsigned fuzzy = 0; + unsigned total = 0; + + FileType localFileType = FileType::NONE; }; + + struct TranslationsModel::Private { QDir m_dir; // initial state is just english - QVector<Language> m_languages = {{defaultLangCode, QLocale(defaultLangCode), false}}; + QVector<Language> m_languages = {Language (defaultLangCode)}; + QString m_selectedLanguage = defaultLangCode; std::unique_ptr<QTranslator> m_qt_translator; std::unique_ptr<QTranslator> m_app_translator; @@ -35,45 +124,285 @@ struct TranslationsModel::Private NetJobPtr m_dl_job; NetJobPtr m_index_job; QString m_nextDownload; + + std::unique_ptr<POTranslator> m_po_translator; + QFileSystemWatcher *watcher; }; TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent) { d.reset(new Private); d->m_dir.setPath(path); - loadLocalIndex(); + FS::ensureFolderPathExists(path); + reloadLocalFiles(); + + d->watcher = new QFileSystemWatcher(this); + connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged); + d->watcher->addPath(d->m_dir.canonicalPath()); } TranslationsModel::~TranslationsModel() { } +void TranslationsModel::translationDirChanged(const QString& path) +{ + qDebug() << "Dir changed:" << path; + reloadLocalFiles(); + selectLanguage(selectedLanguage()); +} + +void TranslationsModel::indexReceived() +{ + qDebug() << "Got translations index!"; + d->m_index_job.reset(); + if(d->m_selectedLanguage != defaultLangCode) + { + downloadTranslation(d->m_selectedLanguage); + } +} + +namespace { +void readIndex(const QString & path, QMap<QString, Language>& languages) +{ + QByteArray data; + try + { + data = FS::read(path); + } + catch (const Exception &e) + { + qCritical() << "Translations Download Failed: index file not readable"; + return; + } + + int index = 1; + try + { + auto toplevel_doc = Json::requireDocument(data); + auto doc = Json::requireObject(toplevel_doc); + auto file_type = Json::requireString(doc, "file_type"); + if(file_type != "MMC-TRANSLATION-INDEX") + { + qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type; + return; + } + auto version = Json::requireInteger(doc, "version"); + if(version > 2) + { + qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type; + return; + } + auto langObjs = Json::requireObject(doc, "languages"); + for(auto iter = langObjs.begin(); iter != langObjs.end(); iter++) + { + Language lang(iter.key()); + + auto langObj = Json::requireObject(iter.value()); + lang.setTranslationStats( + Json::ensureInteger(langObj, "translated", 0), + Json::ensureInteger(langObj, "untranslated", 0), + Json::ensureInteger(langObj, "fuzzy", 0) + ); + lang.file_name = Json::requireString(langObj, "file"); + lang.file_sha1 = Json::requireString(langObj, "sha1"); + lang.file_size = Json::requireInteger(langObj, "size"); + + languages.insert(lang.key, lang); + index++; + } + } + catch (Json::JsonException & e) + { + qCritical() << "Translations Download Failed: index file could not be parsed as json"; + } +} +} + +void TranslationsModel::reloadLocalFiles() +{ + QMap<QString, Language> languages = {{defaultLangCode, Language(defaultLangCode)}}; + + readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages); + auto entries = d->m_dir.entryInfoList({"mmc_*.qm", "*.po"}, QDir::Files | QDir::NoDotAndDotDot); + for(auto & entry: entries) + { + auto completeSuffix = entry.completeSuffix(); + QString langCode; + FileType fileType = FileType::NONE; + if(completeSuffix == "qm") + { + langCode = entry.baseName().remove(0,4); + fileType = FileType::QM; + } + else if(completeSuffix == "po") + { + langCode = entry.baseName(); + fileType = FileType::PO; + } + else + { + continue; + } + + auto langIter = languages.find(langCode); + if(langIter != languages.end()) + { + auto & language = *langIter; + if(int(fileType) > int(language.localFileType)) + { + language.localFileType = fileType; + } + } + else + { + if(fileType == FileType::PO) + { + Language localFound(langCode); + localFound.localFileType = FileType::PO; + languages.insert(langCode, localFound); + } + } + } + + // changed and removed languages + for(auto iter = d->m_languages.begin(); iter != d->m_languages.end();) + { + auto &language = *iter; + auto row = iter - d->m_languages.begin(); + + auto updatedLanguageIter = languages.find(language.key); + if(updatedLanguageIter != languages.end()) + { + if(language.isIdenticalTo(*updatedLanguageIter)) + { + languages.remove(language.key); + } + else + { + language.apply(*updatedLanguageIter); + emit dataChanged(index(row), index(row)); + languages.remove(language.key); + } + iter++; + } + else + { + beginRemoveRows(QModelIndex(), row, row); + iter = d->m_languages.erase(iter); + endRemoveRows(); + } + } + // added languages + if(languages.isEmpty()) + { + return; + } + beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1); + for(auto & language: languages) + { + d->m_languages.append(language); + } + std::sort(d->m_languages.begin(), d->m_languages.end(), [](const Language& a, const Language& b) { + return a.key.compare(b.key) < 0; + }); + endInsertRows(); +} + +namespace { +enum class Column +{ + Language, + Completeness +}; +} + + QVariant TranslationsModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); + auto column = static_cast<Column>(index.column()); if (row < 0 || row >= d->m_languages.size()) return QVariant(); + auto & lang = d->m_languages[row]; switch (role) { case Qt::DisplayRole: - return d->m_languages[row].locale.nativeLanguageName(); + { + switch(column) + { + case Column::Language: + { + return lang.locale.nativeLanguageName(); + } + case Column::Completeness: + { + QString text; + text.sprintf("%3.1f %%", lang.percentTranslated()); + return text; + } + } + } + case Qt::ToolTipRole: + { + return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); + } case Qt::UserRole: - return d->m_languages[row].key; + return lang.key; default: return QVariant(); } } +QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + auto column = static_cast<Column>(section); + if(role == Qt::DisplayRole) + { + switch(column) + { + case Column::Language: + { + return tr("Language"); + } + case Column::Completeness: + { + return tr("Completeness"); + } + } + } + else if(role == Qt::ToolTipRole) + { + switch(column) + { + case Column::Language: + { + return tr("The native language name."); + } + case Column::Completeness: + { + return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones."); + } + } + } + return QAbstractListModel::headerData(section, orientation, role); +} + int TranslationsModel::rowCount(const QModelIndex& parent) const { return d->m_languages.size(); } +int TranslationsModel::columnCount(const QModelIndex& parent) const +{ + return 2; +} + Language * TranslationsModel::findLanguage(const QString& key) { auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang) @@ -121,7 +450,7 @@ bool TranslationsModel::selectLanguage(QString key) * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * This function is not reentrant. */ - QLocale locale(langCode); + QLocale locale = QLocale(langCode); QLocale::setDefault(locale); // if it's the default UI language, finish @@ -153,18 +482,48 @@ bool TranslationsModel::selectLanguage(QString key) d->m_qt_translator.reset(); } - d->m_app_translator.reset(new QTranslator()); - if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) + if(langPtr->localFileType == FileType::PO) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; - if (!QCoreApplication::installTranslator(d->m_app_translator.get())) + auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po")); + if(!poTranslator->isEmpty()) + { + if (!QCoreApplication::installTranslator(poTranslator)) + { + delete poTranslator; + qCritical() << "Installing Application Language File failed."; + } + else + { + d->m_app_translator.reset(poTranslator); + successful = true; + } + } + else { qCritical() << "Loading Application Language File failed."; d->m_app_translator.reset(); } + } + else if(langPtr->localFileType == FileType::QM) + { + d->m_app_translator.reset(new QTranslator()); + if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) + { + qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; + if (!QCoreApplication::installTranslator(d->m_app_translator.get())) + { + qCritical() << "Installing Application Language File failed."; + d->m_app_translator.reset(); + } + else + { + successful = true; + } + } else { - successful = true; + d->m_app_translator.reset(); } } else @@ -199,55 +558,15 @@ void TranslationsModel::downloadIndex() } qDebug() << "Downloading Translations Index..."; d->m_index_job.reset(new NetJob("Translations Index")); - MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "index"); - d->m_index_task = Net::Download::makeCached(QUrl("http://files.multimc.org/translations/index"), entry); + MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "index_v2.json"); + entry->setStale(true); + d->m_index_task = Net::Download::makeCached(QUrl("https://files.multimc.org/translations/index_v2.json"), entry); d->m_index_job->addNetAction(d->m_index_task); connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); - connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexRecieved); + connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); d->m_index_job->start(); } -void TranslationsModel::indexRecieved() -{ - qDebug() << "Got translations index!"; - d->m_index_job.reset(); - loadLocalIndex(); - if(d->m_selectedLanguage != defaultLangCode) - { - downloadTranslation(d->m_selectedLanguage); - } -} - -void TranslationsModel::loadLocalIndex() -{ - QByteArray data; - try - { - data = FS::read(d->m_dir.absoluteFilePath("index")); - } - catch (const Exception &e) - { - qCritical() << "Translations Download Failed: index file not readable"; - return; - } - QVector<Language> languages; - QList<QByteArray> lines = data.split('\n'); - // add the default english. - languages.append({defaultLangCode, QLocale(defaultLangCode), true}); - for (const auto line : lines) - { - if(!line.isEmpty()) - { - auto str = QString::fromLatin1(line); - str.remove(".qm"); - languages.append({str, QLocale(str), false}); - } - } - beginResetModel(); - d->m_languages.swap(languages); - endResetModel(); -} - void TranslationsModel::updateLanguage(QString key) { if(key == defaultLangCode) @@ -274,13 +593,28 @@ void TranslationsModel::downloadTranslation(QString key) d->m_nextDownload = key; return; } + auto lang = findLanguage(key); + if(!lang) + { + qWarning() << "Will not download an unknown translation" << key; + return; + } + d->m_downloadingTranslation = key; MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); entry->setStale(true); + + auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); + auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + dl->m_total_progress = lang->file_size; + d->m_dl_job.reset(new NetJob("Translation for " + key)); - d->m_dl_job->addNetAction(Net::Download::makeCached(QUrl(URLConstants::TRANSLATIONS_BASE_URL + key + ".qm"), entry)); + d->m_dl_job->addNetAction(dl); + connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); + d->m_dl_job->start(); } diff --git a/api/logic/translations/TranslationsModel.h b/api/logic/translations/TranslationsModel.h index 8a9298d4..17e5b124 100644 --- a/api/logic/translations/TranslationsModel.h +++ b/api/logic/translations/TranslationsModel.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -28,8 +28,10 @@ public: explicit TranslationsModel(QString path, QObject *parent = 0); virtual ~TranslationsModel(); - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & parent) const override; bool selectLanguage(QString key); void updateLanguage(QString key); @@ -40,7 +42,7 @@ public: private: Language *findLanguage(const QString & key); - void loadLocalIndex(); + void reloadLocalFiles(); void downloadTranslation(QString key); void downloadNext(); @@ -50,10 +52,12 @@ private: TranslationsModel &operator=(const TranslationsModel &) = delete; private slots: - void indexRecieved(); + void indexReceived(); void indexFailed(QString reason); void dlFailed(QString reason); void dlGood(); + void translationDirChanged(const QString &path); + private: /* data */ struct Private; diff --git a/api/logic/updater/DownloadTask.cpp b/api/logic/updater/DownloadTask.cpp index cb92018d..20b26ebb 100644 --- a/api/logic/updater/DownloadTask.cpp +++ b/api/logic/updater/DownloadTask.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -131,7 +131,14 @@ void DownloadTask::processDownloadedVersionInfo() QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged); QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed); - setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size()))); + if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/MultiMC5/issues/1701 + { + setStatus(tr("Downloading one update file.")); + } + else + { + setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size()))); + } qDebug() << "Begin downloading update files to" << m_updateFilesDir.path(); m_filesNetJob = netJob; m_filesNetJob->start(); diff --git a/api/logic/updater/DownloadTask.h b/api/logic/updater/DownloadTask.h index 30b4b9ec..88e60865 100644 --- a/api/logic/updater/DownloadTask.h +++ b/api/logic/updater/DownloadTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/updater/DownloadTask_test.cpp b/api/logic/updater/DownloadTask_test.cpp index 531b2527..8d5375e8 100644 --- a/api/logic/updater/DownloadTask_test.cpp +++ b/api/logic/updater/DownloadTask_test.cpp @@ -185,25 +185,6 @@ slots: qDebug() << expectedOperations; QCOMPARE(operations, expectedOperations); } - - void test_OSXPathFixup() - { - QString path, pathOrig; - bool result; - // Proper OSX path - pathOrig = path = "MultiMC.app/Foo/Bar/Baz"; - qDebug() << "Proper OSX path: " << path; - result = fixPathForOSX(path); - QCOMPARE(path, QString("Foo/Bar/Baz")); - QCOMPARE(result, true); - - // Bad OSX path - pathOrig = path = "translations/klingon.lol"; - qDebug() << "Bad OSX path: " << path; - result = fixPathForOSX(path); - QCOMPARE(path, pathOrig); - QCOMPARE(result, false); - } }; extern "C" diff --git a/api/logic/updater/GoUpdate.cpp b/api/logic/updater/GoUpdate.cpp index ef040db6..6167418e 100644 --- a/api/logic/updater/GoUpdate.cpp +++ b/api/logic/updater/GoUpdate.cpp @@ -33,13 +33,7 @@ bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &er QJsonObject fileObj = fileValue.toObject(); QString file_path = fileObj.value("Path").toString(); -#ifdef Q_OS_MAC - // On OSX, the paths for the updater need to be fixed. - // basically, anything that isn't in the .app folder is ignored. - // everything else is changed so the code that processes the files actually finds - // them and puts the replacements in the right spots. - fixPathForOSX(file_path); -#endif + VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(), FileSourceList(), fileObj.value("MD5").toString(), }; qDebug() << "File" << file.path << "with perms" << file.mode; @@ -201,19 +195,4 @@ bool processFileLists } return true; } - -bool fixPathForOSX(QString &path) -{ - if (path.startsWith("MultiMC.app/")) - { - // remove the prefix and add a new, more appropriate one. - path.remove(0, 12); - return true; - } - else - { - qCritical() << "Update path not within .app: " << path; - return false; - } } -}
\ No newline at end of file diff --git a/api/logic/updater/GoUpdate.h b/api/logic/updater/GoUpdate.h index 54559a3c..8f92bb99 100644 --- a/api/logic/updater/GoUpdate.h +++ b/api/logic/updater/GoUpdate.h @@ -123,14 +123,5 @@ bool MULTIMC_LOGIC_EXPORT processFileLists OperationList &ops ); -/*! - * This fixes destination paths for OSX - removes 'MultiMC.app' prefix - * The updater runs in MultiMC.app/Contents/MacOs by default - * The destination paths are such as this: MultiMC.app/blah/blah - * - * @return false if the path couldn't be fixed (is invalid) - */ -bool MULTIMC_LOGIC_EXPORT fixPathForOSX(QString &path); - } Q_DECLARE_METATYPE(GoUpdate::Status) diff --git a/api/logic/updater/UpdateChecker.cpp b/api/logic/updater/UpdateChecker.cpp index 9e610d52..be33c73c 100644 --- a/api/logic/updater/UpdateChecker.cpp +++ b/api/logic/updater/UpdateChecker.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/updater/UpdateChecker.h b/api/logic/updater/UpdateChecker.h index bb5013de..20906207 100644 --- a/api/logic/updater/UpdateChecker.h +++ b/api/logic/updater/UpdateChecker.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/updater/UpdateChecker_test.cpp b/api/logic/updater/UpdateChecker_test.cpp index 59f2a5a1..5702d9c6 100644 --- a/api/logic/updater/UpdateChecker_test.cpp +++ b/api/logic/updater/UpdateChecker_test.cpp @@ -21,6 +21,11 @@ QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c) return dbg.maybeSpace(); } +QString findTestDataUrl(const char *file) +{ + return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); +} + class UpdateCheckerTest : public QObject { Q_OBJECT @@ -35,10 +40,6 @@ slots: } - static QString findTestDataUrl(const char *file) - { - return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); - } void tst_ChannelListParsing_data() { QTest::addColumn<QString>("channel"); |