diff options
Diffstat (limited to 'launcher')
70 files changed, 2427 insertions, 1796 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0d1db11c..968dd08e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -78,6 +78,7 @@ #include <iostream> #include <QAccessible> +#include <QCommandLineParser> #include <QDir> #include <QFileInfo> #include <QNetworkAccessManager> @@ -110,7 +111,6 @@ #include "translations/TranslationsModel.h" #include "meta/Index.h" -#include <Commandline.h> #include <FileSystem.h> #include <DesktopServices.h> #include <LocalPeer.h> @@ -136,12 +136,6 @@ static const QLatin1String liveCheckFile("live.check"); -using namespace Commandline; - -#define MACOS_HINT "If you are on macOS Sierra, you might have to move the app to your /Applications or ~/Applications folder. "\ - "This usually fixes the problem and you can move the application elsewhere afterwards.\n"\ - "\n" - namespace { void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -242,80 +236,27 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) this->setQuitOnLastWindowClosed(false); // Commandline parsing - QHash<QString, QVariant> args; - { - Parser parser(FlagStyle::GNU, ArgumentStyle::SpaceAndEquals); - - // --help - parser.addSwitch("help"); - parser.addShortOpt("help", 'h'); - parser.addDocumentation("help", "Display this help and exit."); - // --version - parser.addSwitch("version"); - parser.addShortOpt("version", 'V'); - parser.addDocumentation("version", "Display program version and exit."); - // --dir - parser.addOption("dir"); - parser.addShortOpt("dir", 'd'); - parser.addDocumentation("dir", "Use the supplied folder as application root instead of the binary location (use '.' for current)"); - // --launch - parser.addOption("launch"); - parser.addShortOpt("launch", 'l'); - parser.addDocumentation("launch", "Launch the specified instance (by instance ID)"); - // --server - parser.addOption("server"); - parser.addShortOpt("server", 's'); - parser.addDocumentation("server", "Join the specified server on launch (only valid in combination with --launch)"); - // --profile - parser.addOption("profile"); - parser.addShortOpt("profile", 'a'); - parser.addDocumentation("profile", "Use the account specified by its profile name (only valid in combination with --launch)"); - // --alive - parser.addSwitch("alive"); - parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"); - // --import - parser.addOption("import"); - parser.addShortOpt("import", 'I'); - parser.addDocumentation("import", "Import instance from specified zip (local path or URL)"); - - // parse the arguments - try - { - args = parser.parse(arguments()); - } - catch (const ParsingError &e) - { - std::cerr << "CommandLineError: " << e.what() << std::endl; - if(argc > 0) - std::cerr << "Try '" << argv[0] << " -h' to get help on command line parameters." - << std::endl; - m_status = Application::Failed; - return; - } - - // display help and exit - if (args["help"].toBool()) - { - std::cout << qPrintable(parser.compileHelp(arguments()[0])); - m_status = Application::Succeeded; - return; - } + QCommandLineParser parser; + parser.setApplicationDescription(BuildConfig.LAUNCHER_NAME); + + parser.addOptions({ + {{"d", "dir"}, "Use a custom path as application root (use '.' for current directory)", "directory"}, + {{"l", "launch"}, "Launch the specified instance (by instance ID)", "instance"}, + {{"s", "server"}, "Join the specified server on launch (only valid in combination with --launch)", "address"}, + {{"a", "profile"}, "Use the account specified by its profile name (only valid in combination with --launch)", "profile"}, + {"alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"}, + {{"I", "import"}, "Import instance from specified zip (local path or URL)", "file"} + }); + parser.addHelpOption(); + parser.addVersionOption(); - // display version and exit - if (args["version"].toBool()) - { - std::cout << "Version " << BuildConfig.printableVersionString().toStdString() << std::endl; - std::cout << "Git " << BuildConfig.GIT_COMMIT.toStdString() << std::endl; - m_status = Application::Succeeded; - return; - } - } + parser.process(arguments()); - m_instanceIdToLaunch = args["launch"].toString(); - m_serverToJoin = args["server"].toString(); - m_profileToUse = args["profile"].toString(); - m_liveCheck = args["alive"].toBool(); - m_zipToImport = args["import"].toUrl(); + m_instanceIdToLaunch = parser.value("launch"); + m_serverToJoin = parser.value("server"); + m_profileToUse = parser.value("profile"); + m_liveCheck = parser.isSet("alive"); + m_zipToImport = parser.value("import"); // error if --launch is missing with --server or --profile if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) @@ -346,7 +287,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString adjustedBy; QString dataPath; // change folder - QString dirParam = args["dir"].toString(); + QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes multimc data path point to whatever the user specified @@ -385,9 +326,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString( "The launcher data folder could not be created.\n" "\n" -#if defined(Q_OS_MAC) - MACOS_HINT -#endif "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" "(%1)\n" "\n" @@ -403,9 +341,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString( "The launcher data folder could not be opened.\n" "\n" -#if defined(Q_OS_MAC) - MACOS_HINT -#endif "Make sure you have the right permissions to the launcher data folder.\n" "(%1)\n" "\n" @@ -486,9 +421,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString( "The launcher couldn't create a log file - the data folder is not writable.\n" "\n" - #if defined(Q_OS_MAC) - MACOS_HINT - #endif "Make sure you have write permissions to the data folder.\n" "(%1)\n" "\n" @@ -927,12 +859,13 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "<> Application theme set."; } + updateCapabilities(); + if(createSetupWizard()) { return; } - updateCapabilities(); performMainStartupAction(); } diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 944fd4b2..a0bd8823 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -114,44 +114,54 @@ QString BaseInstance::getPostExitCommand() return settings()->get("PostExitCommand").toString(); } -bool BaseInstance::isManagedPack() +bool BaseInstance::isManagedPack() const { - return settings()->get("ManagedPack").toBool(); + return m_settings->get("ManagedPack").toBool(); } -QString BaseInstance::getManagedPackType() +QString BaseInstance::getManagedPackType() const { - return settings()->get("ManagedPackType").toString(); + return m_settings->get("ManagedPackType").toString(); } -QString BaseInstance::getManagedPackID() +QString BaseInstance::getManagedPackID() const { - return settings()->get("ManagedPackID").toString(); + return m_settings->get("ManagedPackID").toString(); } -QString BaseInstance::getManagedPackName() +QString BaseInstance::getManagedPackName() const { - return settings()->get("ManagedPackName").toString(); + return m_settings->get("ManagedPackName").toString(); } -QString BaseInstance::getManagedPackVersionID() +QString BaseInstance::getManagedPackVersionID() const { - return settings()->get("ManagedPackVersionID").toString(); + return m_settings->get("ManagedPackVersionID").toString(); } -QString BaseInstance::getManagedPackVersionName() +QString BaseInstance::getManagedPackVersionName() const { - return settings()->get("ManagedPackVersionName").toString(); + return m_settings->get("ManagedPackVersionName").toString(); } void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) { - settings()->set("ManagedPack", true); - settings()->set("ManagedPackType", type); - settings()->set("ManagedPackID", id); - settings()->set("ManagedPackName", name); - settings()->set("ManagedPackVersionID", versionId); - settings()->set("ManagedPackVersionName", version); + m_settings->set("ManagedPack", true); + m_settings->set("ManagedPackType", type); + m_settings->set("ManagedPackID", id); + m_settings->set("ManagedPackName", name); + m_settings->set("ManagedPackVersionID", versionId); + m_settings->set("ManagedPackVersionName", version); +} + +void BaseInstance::copyManagedPack(BaseInstance& other) +{ + m_settings->set("ManagedPack", other.isManagedPack()); + m_settings->set("ManagedPackType", other.getManagedPackType()); + m_settings->set("ManagedPackID", other.getManagedPackID()); + m_settings->set("ManagedPackName", other.getManagedPackName()); + m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); + m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); } int BaseInstance::getConsoleMaxLines() const diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index b86401d6..307240e0 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -141,13 +141,14 @@ public: QString getPostExitCommand(); QString getWrapperCommand(); - bool isManagedPack(); - QString getManagedPackType(); - QString getManagedPackID(); - QString getManagedPackName(); - QString getManagedPackVersionID(); - QString getManagedPackVersionName(); + bool isManagedPack() const; + QString getManagedPackType() const; + QString getManagedPackID() const; + QString getManagedPackName() const; + QString getManagedPackVersionID() const; + QString getManagedPackVersionName() const; void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); + void copyManagedPack(BaseInstance& other); /// guess log level from a line of game log virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f7bcb1d8..c5894268 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -296,6 +296,8 @@ set(MINECRAFT_SOURCES minecraft/Library.cpp minecraft/Library.h minecraft/MojangDownloadInfo.h + minecraft/VanillaInstanceCreationTask.cpp + minecraft/VanillaInstanceCreationTask.h minecraft/VersionFile.cpp minecraft/VersionFile.h minecraft/VersionFilterData.h @@ -319,6 +321,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp + minecraft/mod/TexturePack.h + minecraft/mod/TexturePack.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h @@ -331,6 +335,8 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalModUpdateTask.cpp minecraft/mod/tasks/LocalResourcePackParseTask.h minecraft/mod/tasks/LocalResourcePackParseTask.cpp + minecraft/mod/tasks/LocalTexturePackParseTask.h + minecraft/mod/tasks/LocalTexturePackParseTask.cpp # Assets minecraft/AssetsUtils.h @@ -454,6 +460,8 @@ set(API_SOURCES modplatform/helpers/NetworkModAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp + modplatform/helpers/OverrideUtils.h + modplatform/helpers/OverrideUtils.cpp ) set(FTB_SOURCES @@ -479,6 +487,8 @@ set(FLAME_SOURCES modplatform/flame/FileResolvingTask.cpp modplatform/flame/FlameCheckUpdate.cpp modplatform/flame/FlameCheckUpdate.h + modplatform/flame/FlameInstanceCreationTask.h + modplatform/flame/FlameInstanceCreationTask.cpp ) set(MODRINTH_SOURCES @@ -488,6 +498,8 @@ set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackManifest.h modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.h + modplatform/modrinth/ModrinthInstanceCreationTask.cpp + modplatform/modrinth/ModrinthInstanceCreationTask.h ) set(MODPACKSCH_SOURCES @@ -960,7 +972,7 @@ target_link_libraries(Launcher_logic Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} - tomlc99 + tomlplusplus::tomlplusplus BuildConfig Katabasis Qt${QT_VERSION_MAJOR}::Widgets diff --git a/launcher/Commandline.cpp b/launcher/Commandline.cpp index 8e7356bb..6d97918d 100644 --- a/launcher/Commandline.cpp +++ b/launcher/Commandline.cpp @@ -92,412 +92,4 @@ QStringList splitArgs(QString args) argv << current; return argv; } - -Parser::Parser(FlagStyle::Enum flagStyle, ArgumentStyle::Enum argStyle) -{ - m_flagStyle = flagStyle; - m_argStyle = argStyle; -} - -// styles setter/getter -void Parser::setArgumentStyle(ArgumentStyle::Enum style) -{ - m_argStyle = style; -} -ArgumentStyle::Enum Parser::argumentStyle() -{ - return m_argStyle; -} - -void Parser::setFlagStyle(FlagStyle::Enum style) -{ - m_flagStyle = style; -} -FlagStyle::Enum Parser::flagStyle() -{ - return m_flagStyle; -} - -// setup methods -void Parser::addSwitch(QString name, bool def) -{ - if (m_params.contains(name)) - throw "Name not unique"; - - OptionDef *param = new OptionDef; - param->type = otSwitch; - param->name = name; - param->metavar = QString("<%1>").arg(name); - param->def = def; - - m_options[name] = param; - m_params[name] = (CommonDef *)param; - m_optionList.append(param); -} - -void Parser::addOption(QString name, QVariant def) -{ - if (m_params.contains(name)) - throw "Name not unique"; - - OptionDef *param = new OptionDef; - param->type = otOption; - param->name = name; - param->metavar = QString("<%1>").arg(name); - param->def = def; - - m_options[name] = param; - m_params[name] = (CommonDef *)param; - m_optionList.append(param); -} - -void Parser::addArgument(QString name, bool required, QVariant def) -{ - if (m_params.contains(name)) - throw "Name not unique"; - - PositionalDef *param = new PositionalDef; - param->name = name; - param->def = def; - param->required = required; - param->metavar = name; - - m_positionals.append(param); - m_params[name] = (CommonDef *)param; -} - -void Parser::addDocumentation(QString name, QString doc, QString metavar) -{ - if (!m_params.contains(name)) - throw "Name does not exist"; - - CommonDef *param = m_params[name]; - param->doc = doc; - if (!metavar.isNull()) - param->metavar = metavar; -} - -void Parser::addShortOpt(QString name, QChar flag) -{ - if (!m_params.contains(name)) - throw "Name does not exist"; - if (!m_options.contains(name)) - throw "Name is not an Option or Swtich"; - - OptionDef *param = m_options[name]; - m_flags[flag] = param; - param->flag = flag; -} - -// help methods -QString Parser::compileHelp(QString progName, int helpIndent, bool useFlags) -{ - QStringList help; - help << compileUsage(progName, useFlags) << "\r\n"; - - // positionals - if (!m_positionals.isEmpty()) - { - help << "\r\n"; - help << "Positional arguments:\r\n"; - QListIterator<PositionalDef *> it2(m_positionals); - while (it2.hasNext()) - { - PositionalDef *param = it2.next(); - help << " " << param->metavar; - help << " " << QString(helpIndent - param->metavar.length() - 1, ' '); - help << param->doc << "\r\n"; - } - } - - // Options - if (!m_optionList.isEmpty()) - { - help << "\r\n"; - QString optPrefix, flagPrefix; - getPrefix(optPrefix, flagPrefix); - - help << "Options & Switches:\r\n"; - QListIterator<OptionDef *> it(m_optionList); - while (it.hasNext()) - { - OptionDef *option = it.next(); - help << " "; - int nameLength = optPrefix.length() + option->name.length(); - if (!option->flag.isNull()) - { - nameLength += 3 + flagPrefix.length(); - help << flagPrefix << option->flag << ", "; - } - help << optPrefix << option->name; - if (option->type == otOption) - { - QString arg = QString("%1%2").arg( - ((m_argStyle == ArgumentStyle::Equals) ? "=" : " "), option->metavar); - nameLength += arg.length(); - help << arg; - } - help << " " << QString(helpIndent - nameLength - 1, ' '); - help << option->doc << "\r\n"; - } - } - - return help.join(""); -} - -QString Parser::compileUsage(QString progName, bool useFlags) -{ - QStringList usage; - usage << "Usage: " << progName; - - QString optPrefix, flagPrefix; - getPrefix(optPrefix, flagPrefix); - - // options - QListIterator<OptionDef *> it(m_optionList); - while (it.hasNext()) - { - OptionDef *option = it.next(); - usage << " ["; - if (!option->flag.isNull() && useFlags) - usage << flagPrefix << option->flag; - else - usage << optPrefix << option->name; - if (option->type == otOption) - usage << ((m_argStyle == ArgumentStyle::Equals) ? "=" : " ") << option->metavar; - usage << "]"; - } - - // arguments - QListIterator<PositionalDef *> it2(m_positionals); - while (it2.hasNext()) - { - PositionalDef *param = it2.next(); - usage << " " << (param->required ? "<" : "["); - usage << param->metavar; - usage << (param->required ? ">" : "]"); - } - - return usage.join(""); -} - -// parsing -QHash<QString, QVariant> Parser::parse(QStringList argv) -{ - QHash<QString, QVariant> map; - - QStringListIterator it(argv); - QString programName = it.next(); - - QString optionPrefix; - QString flagPrefix; - QListIterator<PositionalDef *> positionals(m_positionals); - QStringList expecting; - - getPrefix(optionPrefix, flagPrefix); - - while (it.hasNext()) - { - QString arg = it.next(); - - if (!expecting.isEmpty()) - // we were expecting an argument - { - QString name = expecting.first(); -/* - if (map.contains(name)) - throw ParsingError( - QString("Option %2%1 was given multiple times").arg(name, optionPrefix)); -*/ - map[name] = QVariant(arg); - - expecting.removeFirst(); - continue; - } - - if (arg.startsWith(optionPrefix)) - // we have an option - { - // qDebug("Found option %s", qPrintable(arg)); - - QString name = arg.mid(optionPrefix.length()); - QString equals; - - if ((m_argStyle == ArgumentStyle::Equals || - m_argStyle == ArgumentStyle::SpaceAndEquals) && - name.contains("=")) - { - int i = name.indexOf("="); - equals = name.mid(i + 1); - name = name.left(i); - } - - if (m_options.contains(name)) - { - /* - if (map.contains(name)) - throw ParsingError(QString("Option %2%1 was given multiple times") - .arg(name, optionPrefix)); -*/ - OptionDef *option = m_options[name]; - if (option->type == otSwitch) - map[name] = true; - else // if (option->type == otOption) - { - if (m_argStyle == ArgumentStyle::Space) - expecting.append(name); - else if (!equals.isNull()) - map[name] = equals; - else if (m_argStyle == ArgumentStyle::SpaceAndEquals) - expecting.append(name); - else - throw ParsingError(QString("Option %2%1 reqires an argument.") - .arg(name, optionPrefix)); - } - - continue; - } - - throw ParsingError(QString("Unknown Option %2%1").arg(name, optionPrefix)); - } - - if (arg.startsWith(flagPrefix)) - // we have (a) flag(s) - { - // qDebug("Found flags %s", qPrintable(arg)); - - QString flags = arg.mid(flagPrefix.length()); - QString equals; - - if ((m_argStyle == ArgumentStyle::Equals || - m_argStyle == ArgumentStyle::SpaceAndEquals) && - flags.contains("=")) - { - int i = flags.indexOf("="); - equals = flags.mid(i + 1); - flags = flags.left(i); - } - - for (int i = 0; i < flags.length(); i++) - { - QChar flag = flags.at(i); - - if (!m_flags.contains(flag)) - throw ParsingError(QString("Unknown flag %2%1").arg(flag, flagPrefix)); - - OptionDef *option = m_flags[flag]; -/* - if (map.contains(option->name)) - throw ParsingError(QString("Option %2%1 was given multiple times") - .arg(option->name, optionPrefix)); -*/ - if (option->type == otSwitch) - map[option->name] = true; - else // if (option->type == otOption) - { - if (m_argStyle == ArgumentStyle::Space) - expecting.append(option->name); - else if (!equals.isNull()) - if (i == flags.length() - 1) - map[option->name] = equals; - else - throw ParsingError(QString("Flag %4%2 of Argument-requiring Option " - "%1 not last flag in %4%3") - .arg(option->name, flag, flags, flagPrefix)); - else if (m_argStyle == ArgumentStyle::SpaceAndEquals) - expecting.append(option->name); - else - throw ParsingError(QString("Option %1 reqires an argument. (flag %3%2)") - .arg(option->name, flag, flagPrefix)); - } - } - - continue; - } - - // must be a positional argument - if (!positionals.hasNext()) - throw ParsingError(QString("Don't know what to do with '%1'").arg(arg)); - - PositionalDef *param = positionals.next(); - - map[param->name] = arg; - } - - // check if we're missing something - if (!expecting.isEmpty()) - throw ParsingError(QString("Was still expecting arguments for %2%1").arg( - expecting.join(QString(", ") + optionPrefix), optionPrefix)); - - while (positionals.hasNext()) - { - PositionalDef *param = positionals.next(); - if (param->required) - throw ParsingError( - QString("Missing required positional argument '%1'").arg(param->name)); - else - map[param->name] = param->def; - } - - // fill out gaps - QListIterator<OptionDef *> iter(m_optionList); - while (iter.hasNext()) - { - OptionDef *option = iter.next(); - if (!map.contains(option->name)) - map[option->name] = option->def; - } - - return map; -} - -// clear defs -void Parser::clear() -{ - m_flags.clear(); - m_params.clear(); - m_options.clear(); - - QMutableListIterator<OptionDef *> it(m_optionList); - while (it.hasNext()) - { - OptionDef *option = it.next(); - it.remove(); - delete option; - } - - QMutableListIterator<PositionalDef *> it2(m_positionals); - while (it2.hasNext()) - { - PositionalDef *arg = it2.next(); - it2.remove(); - delete arg; - } -} - -// Destructor -Parser::~Parser() -{ - clear(); -} - -// getPrefix -void Parser::getPrefix(QString &opt, QString &flag) -{ - if (m_flagStyle == FlagStyle::Windows) - opt = flag = "/"; - else if (m_flagStyle == FlagStyle::Unix) - opt = flag = "-"; - // else if (m_flagStyle == FlagStyle::GNU) - else - { - opt = "--"; - flag = "-"; - } -} - -// ParsingError -ParsingError::ParsingError(const QString &what) : std::runtime_error(what.toStdString()) -{ -} } diff --git a/launcher/Commandline.h b/launcher/Commandline.h index a4e7aa61..8bd79180 100644 --- a/launcher/Commandline.h +++ b/launcher/Commandline.h @@ -17,12 +17,7 @@ #pragma once -#include <exception> -#include <stdexcept> - #include <QString> -#include <QVariant> -#include <QHash> #include <QStringList> /** @@ -39,212 +34,4 @@ namespace Commandline * @return a QStringList containing all arguments */ QStringList splitArgs(QString args); - -/** - * @brief The FlagStyle enum - * Specifies how flags are decorated - */ - -namespace FlagStyle -{ -enum Enum -{ - GNU, /**< --option and -o (GNU Style) */ - Unix, /**< -option and -o (Unix Style) */ - Windows, /**< /option and /o (Windows Style) */ -#ifdef Q_OS_WIN32 - Default = Windows -#else - Default = GNU -#endif -}; -} - -/** - * @brief The ArgumentStyle enum - */ -namespace ArgumentStyle -{ -enum Enum -{ - Space, /**< --option value */ - Equals, /**< --option=value */ - SpaceAndEquals, /**< --option[= ]value */ -#ifdef Q_OS_WIN32 - Default = Equals -#else - Default = SpaceAndEquals -#endif -}; -} - -/** - * @brief The ParsingError class - */ -class ParsingError : public std::runtime_error -{ -public: - ParsingError(const QString &what); -}; - -/** - * @brief The Parser class - */ -class Parser -{ -public: - /** - * @brief Parser constructor - * @param flagStyle the FlagStyle to use in this Parser - * @param argStyle the ArgumentStyle to use in this Parser - */ - Parser(FlagStyle::Enum flagStyle = FlagStyle::Default, - ArgumentStyle::Enum argStyle = ArgumentStyle::Default); - - /** - * @brief set the flag style - * @param style - */ - void setFlagStyle(FlagStyle::Enum style); - - /** - * @brief get the flag style - * @return - */ - FlagStyle::Enum flagStyle(); - - /** - * @brief set the argument style - * @param style - */ - void setArgumentStyle(ArgumentStyle::Enum style); - - /** - * @brief get the argument style - * @return - */ - ArgumentStyle::Enum argumentStyle(); - - /** - * @brief define a boolean switch - * @param name the parameter name - * @param def the default value - */ - void addSwitch(QString name, bool def = false); - - /** - * @brief define an option that takes an additional argument - * @param name the parameter name - * @param def the default value - */ - void addOption(QString name, QVariant def = QVariant()); - - /** - * @brief define a positional argument - * @param name the parameter name - * @param required wether this argument is required - * @param def the default value - */ - void addArgument(QString name, bool required = true, QVariant def = QVariant()); - - /** - * @brief adds a flag to an existing parameter - * @param name the (existing) parameter name - * @param flag the flag character - * @see addSwitch addArgument addOption - * Note: any one parameter can only have one flag - */ - void addShortOpt(QString name, QChar flag); - - /** - * @brief adds documentation to a Parameter - * @param name the parameter name - * @param metavar a string to be displayed as placeholder for the value - * @param doc a QString containing the documentation - * Note: on positional arguments, metavar replaces the name as displayed. - * on options , metavar replaces the value placeholder - */ - void addDocumentation(QString name, QString doc, QString metavar = QString()); - - /** - * @brief generate a help message - * @param progName the program name to use in the help message - * @param helpIndent how much the parameter documentation should be indented - * @param flagsInUsage whether we should use flags instead of options in the usage - * @return a help message - */ - QString compileHelp(QString progName, int helpIndent = 22, bool flagsInUsage = true); - - /** - * @brief generate a short usage message - * @param progName the program name to use in the usage message - * @param useFlags whether we should use flags instead of options - * @return a usage message - */ - QString compileUsage(QString progName, bool useFlags = true); - - /** - * @brief parse - * @param argv a QStringList containing the program ARGV - * @return a QHash mapping argument names to their values - */ - QHash<QString, QVariant> parse(QStringList argv); - - /** - * @brief clear all definitions - */ - void clear(); - - ~Parser(); - -private: - FlagStyle::Enum m_flagStyle; - ArgumentStyle::Enum m_argStyle; - - enum OptionType - { - otSwitch, - otOption - }; - - // Important: the common part MUST BE COMMON ON ALL THREE structs - struct CommonDef - { - QString name; - QString doc; - QString metavar; - QVariant def; - }; - - struct OptionDef - { - // common - QString name; - QString doc; - QString metavar; - QVariant def; - // option - OptionType type; - QChar flag; - }; - - struct PositionalDef - { - // common - QString name; - QString doc; - QString metavar; - QVariant def; - // positional - bool required; - }; - - QHash<QString, OptionDef *> m_options; - QHash<QChar, OptionDef *> m_flags; - QHash<QString, CommonDef *> m_params; - QList<PositionalDef *> m_positionals; - QList<OptionDef *> m_optionList; - - void getPrefix(QString &opt, QString &flag); -}; } diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index c2bfe839..b1e33884 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -44,7 +44,7 @@ void InstanceCopyTask::copyFinished() auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); - inst->setName(m_instName); + inst->setName(name()); inst->setIconKey(m_instIcon); if(!m_keepPlaytime) { inst->resetTimePlayed(); diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index e01bf306..3971effa 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -1,40 +1,56 @@ #include "InstanceCreationTask.h" -#include "settings/INISettingsObject.h" -#include "FileSystem.h" -//FIXME: remove this -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include <QDebug> +#include <QFile> -InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version) -{ - m_version = version; - m_usingLoader = false; -} - -InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion) -{ - m_version = version; - m_usingLoader = true; - m_loader = loader; - m_loaderVersion = loaderVersion; -} +InstanceCreationTask::InstanceCreationTask() = default; void InstanceCreationTask::executeTask() { - setStatus(tr("Creating instance from version %1").arg(m_version->name())); - { - auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); - instanceSettings->suspendSave(); - MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); - auto components = inst.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", m_version->descriptor(), true); - if(m_usingLoader) - components->setComponentVersion(m_loader, m_loaderVersion->descriptor()); - inst.setName(m_instName); - inst.setIconKey(m_instIcon); - instanceSettings->resumeSave(); + setAbortable(true); + + if (updateInstance()) { + emitSucceeded(); + return; + } + + // When the user aborted in the update stage. + if (m_abort) { + emitAborted(); + return; } + + if (!createInstance()) { + if (m_abort) + return; + + qWarning() << "Instance creation failed!"; + if (!m_error_message.isEmpty()) + qWarning() << "Reason: " << m_error_message; + emitFailed(tr("Error while creating new instance.")); + return; + } + + // If this is set, it means we're updating an instance. So, we now need to remove the + // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd + // put the instance in an invalid state. + if (shouldOverride()) { + setAbortable(false); + setStatus(tr("Removing old conflicting files...")); + qDebug() << "Removing old files"; + + for (auto path : m_files_to_remove) { + if (!QFile::exists(path)) + continue; + qDebug() << "Removing" << path; + if (!QFile::remove(path)) { + qCritical() << "Couldn't remove the old conflicting files."; + emitFailed(tr("Failed to remove old conflicting files.")); + return; + } + } + } + emitSucceeded(); + return; } diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h index 23367c3f..03ee1a7a 100644 --- a/launcher/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -1,26 +1,46 @@ #pragma once -#include "tasks/Task.h" -#include "net/NetJob.h" -#include <QUrl> -#include "settings/SettingsObject.h" #include "BaseVersion.h" #include "InstanceTask.h" -class InstanceCreationTask : public InstanceTask -{ +class InstanceCreationTask : public InstanceTask { Q_OBJECT -public: - explicit InstanceCreationTask(BaseVersionPtr version); - explicit InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion); - -protected: - //! Entry point for tasks. - virtual void executeTask() override; - -private: /* data */ - BaseVersionPtr m_version; - bool m_usingLoader; - QString m_loader; - BaseVersionPtr m_loaderVersion; + public: + InstanceCreationTask(); + virtual ~InstanceCreationTask() = default; + + protected: + void executeTask() final override; + + /** + * Tries to update an already existing instance. + * + * This can be implemented by subclasses to provide a way of updating an already existing + * instance, according to that implementation's concept of 'identity' (i.e. instances that + * are updates / downgrades of one another). + * + * If this returns true, createInstance() will not run, so you should do all update steps in here. + * Otherwise, createInstance() is run as normal. + */ + virtual bool updateInstance() { return false; }; + + /** + * Creates a new instance. + * + * Returns whether the instance creation was successful (true) or not (false). + */ + virtual bool createInstance() { return false; }; + + QString getError() const { return m_error_message; } + + protected: + void setError(QString message) { m_error_message = message; }; + + protected: + bool m_abort = false; + + QStringList m_files_to_remove; + + private: + QString m_error_message; }; diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index de0afc96..b490620d 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -35,35 +35,26 @@ */ #include "InstanceImportTask.h" -#include <QtConcurrentRun> + #include "Application.h" -#include "BaseInstance.h" #include "FileSystem.h" #include "MMCZip.h" #include "NullInstance.h" + #include "icons/IconList.h" #include "icons/IconUtils.h" -#include "settings/INISettingsObject.h" -// FIXME: this does not belong here, it's Minecraft/Flame specific -#include <quazip/quazipdir.h> -#include "Json.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" -#include "modplatform/flame/FileResolvingTask.h" -#include "modplatform/flame/PackManifest.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/technic/TechnicPackProcessor.h" +#include "modplatform/modrinth/ModrinthInstanceCreationTask.h" +#include "modplatform/flame/FlameInstanceCreationTask.h" -#include "Application.h" -#include "icons/IconList.h" -#include "net/ChecksumValidator.h" - -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/BlockedModsDialog.h" +#include "settings/INISettingsObject.h" +#include <QtConcurrentRun> #include <algorithm> +#include <quazip/quazipdir.h> + InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) { m_sourceUrl = sourceUrl; @@ -72,35 +63,41 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) bool InstanceImportTask::abort() { + if (!canAbort()) + return false; + if (m_filesNetJob) m_filesNetJob->abort(); m_extractFuture.cancel(); - return false; + return Task::abort(); } void InstanceImportTask::executeTask() { - if (m_sourceUrl.isLocalFile()) - { + setAbortable(true); + + if (m_sourceUrl.isLocalFile()) { m_archivePath = m_sourceUrl.toLocalFile(); processZipPack(); - } - else - { + } else { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); m_downloadRequired = true; - const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); + m_archivePath = entry->getFullPath(); + m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); - m_archivePath = entry->getFullPath(); - auto job = m_filesNetJob.get(); - connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); - connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); - connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed); + + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); + connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); + m_filesNetJob->start(); } } @@ -119,7 +116,13 @@ void InstanceImportTask::downloadFailed(QString reason) void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) { - setProgress(current / 2, total); + setProgress(current, total); +} + +void InstanceImportTask::downloadAborted() +{ + emitAborted(); + m_filesNetJob.reset(); } void InstanceImportTask::processZipPack() @@ -255,293 +258,31 @@ void InstanceImportTask::extractFinished() void InstanceImportTask::extractAborted() { - emitFailed(tr("Instance import has been aborted.")); - return; + emitAborted(); } void InstanceImportTask::processFlame() { - const static QMap<QString,QString> forgemap = { - {"1.2.5", "3.4.9.171"}, - {"1.4.2", "6.0.1.355"}, - {"1.4.7", "6.6.2.534"}, - {"1.5.2", "7.8.1.737"} - }; - Flame::Manifest pack; - try - { - QString configPath = FS::PathCombine(m_stagingPath, "manifest.json"); - Flame::loadManifest(pack, configPath); - QFile::remove(configPath); - } - catch (const JSONValidationError &e) - { - emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); - return; - } - if(!pack.overrides.isEmpty()) - { - QString overridePath = FS::PathCombine(m_stagingPath, pack.overrides); - if (QFile::exists(overridePath)) - { - QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); - if (!QFile::rename(overridePath, mcPath)) - { - emitFailed(tr("Could not rename the overrides folder:\n") + pack.overrides); - return; - } - } - else - { - logWarning(tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(pack.overrides)); - } - } - - QString forgeVersion; - QString fabricVersion; - // TODO: is Quilt relevant here? - for(auto &loader: pack.minecraft.modLoaders) - { - auto id = loader.id; - if(id.startsWith("forge-")) - { - id.remove("forge-"); - forgeVersion = id; - continue; - } - if(id.startsWith("fabric-")) - { - id.remove("fabric-"); - fabricVersion = id; - continue; - } - logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); - } + auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent); - QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared<INISettingsObject>(configPath); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); - auto mcVersion = pack.minecraft.version; - // Hack to correct some 'special sauce'... - if(mcVersion.endsWith('.')) - { - mcVersion.remove(QRegularExpression("[.]+$")); - logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); - } - auto components = instance.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", mcVersion, true); - if(!forgeVersion.isEmpty()) - { - // FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata. - if(forgeVersion == "recommended") - { - if(forgemap.contains(mcVersion)) - { - forgeVersion = forgemap[mcVersion]; - } - else - { - logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion)); - } - } - components->setComponentVersion("net.minecraftforge", forgeVersion); - } - if(!fabricVersion.isEmpty()) - { - components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); - } - if (m_instIcon != "default") - { - instance.setIconKey(m_instIcon); - } - else - { - if(pack.name.contains("Direwolf20")) - { - instance.setIconKey("steve"); - } - else if(pack.name.contains("FTB") || pack.name.contains("Feed The Beast")) - { - instance.setIconKey("ftb_logo"); - } - else - { - // default to something other than the MultiMC default to distinguish these - instance.setIconKey("flame"); - } - } - QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); - QFileInfo jarmodsInfo(jarmodsPath); - if(jarmodsInfo.isDir()) - { - // install all the jar mods - qDebug() << "Found jarmods:"; - QDir jarmodsDir(jarmodsPath); - QStringList jarMods; - for (auto info: jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) - { - qDebug() << info.fileName(); - jarMods.push_back(info.absoluteFilePath()); - } - auto profile = instance.getPackProfile(); - profile->installJarMods(jarMods); - // nuke the original files - FS::deletePath(jarmodsPath); - } - instance.setName(m_instName); - m_modIdResolver = new Flame::FileResolvingTask(APPLICATION->network(), pack); - connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]() - { - auto results = m_modIdResolver->getResults(); - //first check for blocked mods - QString text; - QList<QUrl> urls; - auto anyBlocked = false; - for(const auto& result: results.files.values()) { - if (!result.resolved || result.url.isEmpty()) { - text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl); - urls.append(QUrl(result.websiteUrl)); - anyBlocked = true; - } - } - if(anyBlocked) { - qWarning() << "Blocked mods found, displaying mod list"; - - auto message_dialog = new BlockedModsDialog(m_parent, - tr("Blocked mods found"), - tr("The following mods were blocked on third party launchers.<br/>" - "You will need to manually download them and add them to the modpack"), - text, - urls); - message_dialog->setModal(true); - - if (message_dialog->exec()) { - m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for (const auto &result: m_modIdResolver->getResults().files) { - QString filename = result.fileName; - if (!result.required) { - filename += ".disabled"; - } - - auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); - auto path = FS::PathCombine(m_stagingPath, relpath); - - switch (result.type) { - case Flame::File::Type::Folder: { - logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fall-through intentional, we treat these as plain old mods and dump them wherever. - } - case Flame::File::Type::SingleFile: - case Flame::File::Type::Mod: { - if (!result.url.isEmpty()) { - qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::Download::makeFile(result.url, path); - m_filesNetJob->addNetAction(dl); - } - break; - } - case Flame::File::Type::Modpack: - logWarning( - tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg( - relpath)); - break; - case Flame::File::Type::Cmod2: - case Flame::File::Type::Ctoc: - case Flame::File::Type::Unknown: - logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); - break; - } - } - m_modIdResolver.reset(); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { - m_filesNetJob.reset(); - emitSucceeded(); - } - ); - connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { - m_filesNetJob.reset(); - emitFailed(reason); - }); - connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { - setProgress(current, total); - }); - setStatus(tr("Downloading mods...")); - m_filesNetJob->start(); - } else { - m_modIdResolver.reset(); - emitFailed("Canceled"); - } - } else { - //TODO extract to function ? - m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for (const auto &result: m_modIdResolver->getResults().files) { - QString filename = result.fileName; - if (!result.required) { - filename += ".disabled"; - } - - auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); - auto path = FS::PathCombine(m_stagingPath, relpath); - - switch (result.type) { - case Flame::File::Type::Folder: { - logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fall-through intentional, we treat these as plain old mods and dump them wherever. - } - case Flame::File::Type::SingleFile: - case Flame::File::Type::Mod: { - if (!result.url.isEmpty()) { - qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::Download::makeFile(result.url, path); - m_filesNetJob->addNetAction(dl); - } - break; - } - case Flame::File::Type::Modpack: - logWarning( - tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg( - relpath)); - break; - case Flame::File::Type::Cmod2: - case Flame::File::Type::Ctoc: - case Flame::File::Type::Unknown: - logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); - break; - } - } - m_modIdResolver.reset(); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { - m_filesNetJob.reset(); - emitSucceeded(); - } - ); - connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { - m_filesNetJob.reset(); - emitFailed(reason); - }); - connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { - setProgress(current, total); - }); - setStatus(tr("Downloading mods...")); - m_filesNetJob->start(); - } - } - ); - connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) - { - m_modIdResolver.reset(); - emitFailed(tr("Unable to resolve mod IDs:\n") + reason); - }); - connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, [&](qint64 current, qint64 total) - { - setProgress(current, total); - }); - connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, [&](QString status) - { - setStatus(status); + inst_creation_task->setName(*this); + inst_creation_task->setIcon(m_instIcon); + inst_creation_task->setGroup(m_instGroup); + + connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { + setOverride(inst_creation_task->shouldOverride()); + emitSucceeded(); }); - m_modIdResolver->start(); + connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); + + connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); + connect(inst_creation_task, &Task::aborted, this, &Task::abort); + connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable); + + inst_creation_task->start(); } void InstanceImportTask::processTechnic() @@ -549,7 +290,7 @@ 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); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); } void InstanceImportTask::processMultiMC() @@ -563,7 +304,7 @@ void InstanceImportTask::processMultiMC() instance.resetTimePlayed(); // set a new nice name - instance.setName(m_instName); + instance.setName(name()); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon != "default") { @@ -584,198 +325,26 @@ void InstanceImportTask::processMultiMC() emitSucceeded(); } -// https://docs.modrinth.com/docs/modpacks/format_definition/ void InstanceImportTask::processModrinth() { - std::vector<Modrinth::File> files; - QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; - try { - QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); - auto doc = Json::requireDocument(indexPath); - auto obj = Json::requireObject(doc, "modrinth.index.json"); - int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); - if (formatVersion == 1) { - auto game = Json::requireString(obj, "game", "modrinth.index.json"); - if (game != "minecraft") { - throw JSONValidationError("Unknown game: " + game); - } + auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString()); - auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); - bool had_optional = false; - for (auto modInfo : jsonFiles) { - Modrinth::File file; - file.path = Json::requireString(modInfo, "path"); - - auto env = Json::ensureObject(modInfo, "env"); - // 'env' field is optional - if (!env.isEmpty()) { - QString support = Json::ensureString(env, "client", "unsupported"); - if (support == "unsupported") { - continue; - } else if (support == "optional") { - // TODO: Make a review dialog for choosing which ones the user wants! - if (!had_optional) { - had_optional = true; - auto info = CustomMessageBox::selectable( - m_parent, tr("Optional mod detected!"), - tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), - QMessageBox::Information); - info->exec(); - } - - if (file.path.endsWith(".jar")) - file.path += ".disabled"; - } - } - - QJsonObject hashes = Json::requireObject(modInfo, "hashes"); - QString hash; - QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; - if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha512"); - hashAlgorithm = QCryptographicHash::Sha512; - if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; - if (hash.isEmpty()) { - throw JSONValidationError("No hash found for: " + file.path); - } - } - } - file.hash = QByteArray::fromHex(hash.toLatin1()); - file.hashAlgorithm = hashAlgorithm; - - // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode - // (as Modrinth seems to incorrectly handle spaces) - - auto download_arr = Json::ensureArray(modInfo, "downloads"); - for(auto download : download_arr) { - qWarning() << download.toString(); - bool is_last = download.toString() == download_arr.last().toString(); - - auto download_url = QUrl(download.toString()); - - if (!download_url.isValid()) { - qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL") - .arg(download_url.toString(), file.path); - if(is_last && file.downloads.isEmpty()) - throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); - } - else { - file.downloads.push_back(download_url); - } - } - - files.push_back(file); - } - - auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); - for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { - QString name = it.key(); - if (name == "minecraft") { - minecraftVersion = Json::requireString(*it, "Minecraft version"); - } - else if (name == "fabric-loader") { - fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } - else if (name == "quilt-loader") { - quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } - else if (name == "forge") { - forgeVersion = Json::requireString(*it, "Forge version"); - } - else { - throw JSONValidationError("Unknown dependency type: " + name); - } - } - } else { - throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); - } - QFile::remove(indexPath); - } catch (const JSONValidationError& e) { - emitFailed(tr("Could not understand pack index:\n") + e.cause()); - return; - } + inst_creation_task->setName(*this); + inst_creation_task->setIcon(m_instIcon); + inst_creation_task->setGroup(m_instGroup); - auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - - auto override_path = FS::PathCombine(m_stagingPath, "overrides"); - if (QFile::exists(override_path)) { - if (!QFile::rename(override_path, mcPath)) { - emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); - return; - } - } - - // Do client overrides - auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); - if (QFile::exists(client_override_path)) { - if (!FS::overrideFolder(mcPath, client_override_path)) { - emitFailed(tr("Could not rename the client overrides folder:\n") + "client overrides"); - return; - } - } + connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { + setOverride(inst_creation_task->shouldOverride()); + emitSucceeded(); + }); + connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); - QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared<INISettingsObject>(configPath); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); - auto components = instance.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", minecraftVersion, true); - if (!fabricVersion.isEmpty()) - components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); - if (!quiltVersion.isEmpty()) - components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion); - if (!forgeVersion.isEmpty()) - components->setComponentVersion("net.minecraftforge", forgeVersion); - if (m_instIcon != "default") - { - instance.setIconKey(m_instIcon); - } - else - { - instance.setIconKey("modrinth"); - } - instance.setName(m_instName); - instance.saveNow(); + connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); + connect(inst_creation_task, &Task::aborted, this, &Task::abort); + connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable); - m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for (auto file : files) - { - auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); - qDebug() << "Will try to download" << file.downloads.front() << "to" << path; - auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); - dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_filesNetJob->addNetAction(dl); - - if (file.downloads.size() > 0) { - // FIXME: This really needs to be put into a ConcurrentTask of - // MultipleOptionsTask's , once those exist :) - connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{ - auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); - dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_filesNetJob->addNetAction(dl); - dl->succeeded(); - }); - } - } - connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() - { - m_filesNetJob.reset(); - emitSucceeded(); - } - ); - connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason) - { - m_filesNetJob.reset(); - emitFailed(reason); - }); - connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) - { - setProgress(current, total); - }); - setStatus(tr("Downloading mods...")); - m_filesNetJob->start(); + inst_creation_task->start(); } diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 48ba2161..ef70c819 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -58,7 +58,6 @@ class InstanceImportTask : public InstanceTask public: explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); - bool canAbort() const override { return true; } bool abort() override; const QVector<Flame::File> &getBlockedFiles() const { @@ -80,6 +79,7 @@ private slots: void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); + void downloadAborted(); void extractFinished(); void extractAborted(); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 4447a17c..cebd70d7 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -535,7 +535,20 @@ InstancePtr InstanceList::getInstanceById(QString instId) const return InstancePtr(); } -QModelIndex InstanceList::getInstanceIndexById(const QString& id) const +InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const +{ + if (managed_name.isEmpty()) + return {}; + + for (auto instance : m_instances) { + if (instance->getManagedPackName() == managed_name) + return instance; + } + + return {}; +} + +QModelIndex InstanceList::getInstanceIndexById(const QString &id) const { return index(getInstIndex(getInstanceById(id).get())); } @@ -764,21 +777,17 @@ class InstanceStaging : public Task { Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; - public: - InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) - : backoff(minBackoff, maxBackoff) + InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName) + : m_parent(parent), backoff(minBackoff, maxBackoff), m_stagingPath(std::move(stagingPath)), m_instance_name(std::move(instanceName)), m_groupName(std::move(groupName)) { - m_parent = parent; m_child.reset(child); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); + connect(child, &Task::aborted, this, &InstanceStaging::childAborted); + connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::progress, this, &InstanceStaging::setProgress); - m_instanceName = instanceName; - m_groupName = groupName; - m_stagingPath = stagingPath; - m_backoffTimer.setSingleShot(true); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); } @@ -787,17 +796,16 @@ class InstanceStaging : public Task { // 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; + if (!canAbort()) + return false; + + m_child->abort(); + + return Task::abort(); } bool canAbort() const override { - if (m_child && m_child->canAbort()) { - return true; - } - return false; + return (m_child && m_child->canAbort()); } protected: @@ -808,7 +816,8 @@ class InstanceStaging : public Task { void childSucceded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) { + if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride())) + { emitSucceeded(); return; } @@ -817,7 +826,7 @@ class InstanceStaging : public Task { emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } - qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; + qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } void childFailed(const QString& reason) @@ -826,7 +835,13 @@ class InstanceStaging : public Task { emitFailed(reason); } - private: + void childAborted() + { + emitAborted(); + } + +private: + InstanceList * m_parent; /* * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * Basically, it starts messing things up while the launcher is extracting/creating instances @@ -834,9 +849,8 @@ class InstanceStaging : public Task { */ ExponentialSeries backoff; QString m_stagingPath; - InstanceList* m_parent; - unique_qobject_ptr<Task> m_child; - QString m_instanceName; + unique_qobject_ptr<InstanceTask> m_child; + InstanceName m_instance_name; QString m_groupName; QTimer m_backoffTimer; }; @@ -846,7 +860,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task) auto stagingPath = getStagedInstancePath(); task->setStagingPath(stagingPath); task->setParentSettings(m_globalSettings); - return new InstanceStaging(this, task, stagingPath, task->name(), task->group()); + return new InstanceStaging(this, task, stagingPath, *task, task->group()); } QString InstanceList::getStagedInstancePath() @@ -866,23 +880,50 @@ QString InstanceList::getStagedInstancePath() return path; } -bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName) +bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override) { QDir dir; - QString instID = FS::DirNameFromString(instanceName, m_instDir); + QString instID; + InstancePtr inst; + + if (should_override) { + // This is to avoid problems when the instance folder gets manually renamed + if ((inst = getInstanceByManagedName(instanceName.originalName()))) { + instID = QFileInfo(inst->instanceRoot()).fileName(); + } else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) { + instID = QFileInfo(inst->instanceRoot()).fileName(); + } else { + instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-'); + } + } else { + instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); + } + { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); - if (!dir.rename(path, destination)) { - qWarning() << "Failed to move" << path << "to" << destination; - return false; + + if (should_override) { + if (!FS::overrideFolder(destination, path)) { + qWarning() << "Failed to override" << path << "to" << destination; + return false; + } + } else { + if (!dir.rename(path, destination)) { + qWarning() << "Failed to move" << path << "to" << destination; + return false; + } + + m_instanceGroupIndex[instID] = groupName; + m_groupNameCache.insert(groupName); } - m_instanceGroupIndex[instID] = groupName; + instanceSet.insert(instID); - m_groupNameCache.insert(groupName); + emit instancesChanged(); emit instanceSelectRequest(instID); } + saveGroupList(); return true; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 62282f04..3673298f 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -24,10 +24,10 @@ #include "BaseInstance.h" -#include "QObjectPtr.h" - class QFileSystemWatcher; class InstanceTask; +struct InstanceName; + using InstanceId = QString; using GroupId = QString; using InstanceLocator = std::pair<InstancePtr, int>; @@ -101,7 +101,10 @@ public: InstListError loadList(); void saveNow(); + /* O(n) */ InstancePtr getInstanceById(QString id) const; + /* O(n) */ + InstancePtr getInstanceByManagedName(const QString& managed_name) const; QModelIndex getInstanceIndexById(const QString &id) const; QStringList getGroups(); bool isGroupCollapsed(const QString &groupName); @@ -127,8 +130,10 @@ public: /** * Commit the staging area given by @keyPath to the provider - used when creation succeeds. * Used by instance manipulation tasks. + * should_override is used when another similar instance already exists, and we want to override it + * - for instance, when updating it. */ - bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName); + bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override); /** * Destroy a previously created staging area given by @keyPath - used when creation fails. diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index dd132877..55a44fd3 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,9 +1,52 @@ #include "InstanceTask.h" -InstanceTask::InstanceTask() +#include "ui/dialogs/CustomMessageBox.h" + +InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) +{ + auto dialog = + CustomMessageBox::selectable(parent, QObject::tr("Change instance name"), + QObject::tr("The instance's name seems to include the old version. Would you like to update it?\n\n" + "Old name: %1\n" + "New name: %2") + .arg(old_name, new_name), + QMessageBox::Question, QMessageBox::No | QMessageBox::Yes); + auto result = dialog->exec(); + + if (result == QMessageBox::Yes) + return InstanceNameChange::ShouldChange; + return InstanceNameChange::ShouldKeep; +} + +QString InstanceName::name() const +{ + if (!m_modified_name.isEmpty()) + return modifiedName(); + return QString("%1 %2").arg(m_original_name, m_original_version); +} + +QString InstanceName::originalName() const { + return m_original_name; } -InstanceTask::~InstanceTask() +QString InstanceName::modifiedName() const { + if (!m_modified_name.isEmpty()) + return m_modified_name; + return m_original_name; } + +QString InstanceName::version() const +{ + return m_original_version; +} + +void InstanceName::setName(InstanceName& other) +{ + m_original_name = other.m_original_name; + m_original_version = other.m_original_version; + m_modified_name = other.m_modified_name; +} + +InstanceTask::InstanceTask() : Task(), InstanceName() {} diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index 82e23f11..e35533fc 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -1,52 +1,57 @@ #pragma once -#include "tasks/Task.h" #include "settings/SettingsObject.h" +#include "tasks/Task.h" + +/* Helpers */ +enum class InstanceNameChange { ShouldChange, ShouldKeep }; +[[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); + +struct InstanceName { + public: + InstanceName() = default; + InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {} + + [[nodiscard]] QString modifiedName() const; + [[nodiscard]] QString originalName() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString version() const; + + void setName(QString name) { m_modified_name = name; } + void setName(InstanceName& other); + + protected: + QString m_original_name; + QString m_original_version; + + QString m_modified_name; +}; -class InstanceTask : public Task -{ +class InstanceTask : public Task, public InstanceName { Q_OBJECT -public: - explicit InstanceTask(); - virtual ~InstanceTask(); - - void setParentSettings(SettingsObjectPtr settings) - { - m_globalSettings = settings; - } - - void setStagingPath(const QString &stagingPath) - { - m_stagingPath = stagingPath; - } - - void setName(const QString &name) - { - m_instName = name; - } - QString name() const - { - return m_instName; - } - - void setIcon(const QString &icon) - { - m_instIcon = icon; - } - - void setGroup(const QString &group) - { - m_instGroup = group; - } - QString group() const - { - return m_instGroup; - } - -protected: /* data */ + public: + InstanceTask(); + ~InstanceTask() override = default; + + void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; } + + void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; } + + void setIcon(const QString& icon) { m_instIcon = icon; } + + void setGroup(const QString& group) { m_instGroup = group; } + QString group() const { return m_instGroup; } + + bool shouldOverride() const { return m_override_existing; } + + protected: + void setOverride(bool override) { m_override_existing = override; } + + protected: /* data */ SettingsObjectPtr m_globalSettings; - QString m_instName; QString m_instIcon; QString m_instGroup; QString m_stagingPath; + + bool m_override_existing = false; }; diff --git a/launcher/minecraft/VanillaInstanceCreationTask.cpp b/launcher/minecraft/VanillaInstanceCreationTask.cpp new file mode 100644 index 00000000..c45daa9a --- /dev/null +++ b/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -0,0 +1,34 @@ +#include "VanillaInstanceCreationTask.h" + +#include <utility> + +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" + +VanillaCreationTask::VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version) + : InstanceCreationTask(), m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loader_version)) +{} + +bool VanillaCreationTask::createInstance() +{ + setStatus(tr("Creating instance from version %1").arg(m_version->name())); + + auto instance_settings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); + instance_settings->suspendSave(); + { + MinecraftInstance inst(m_globalSettings, instance_settings, m_stagingPath); + auto components = inst.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if(m_using_loader) + components->setComponentVersion(m_loader, m_loader_version->descriptor()); + + inst.setName(name()); + inst.setIconKey(m_instIcon); + } + instance_settings->resumeSave(); + + return true; +} diff --git a/launcher/minecraft/VanillaInstanceCreationTask.h b/launcher/minecraft/VanillaInstanceCreationTask.h new file mode 100644 index 00000000..7a37bbd6 --- /dev/null +++ b/launcher/minecraft/VanillaInstanceCreationTask.h @@ -0,0 +1,22 @@ +#pragma once + +#include "InstanceCreationTask.h" + +#include <utility> + +class VanillaCreationTask final : public InstanceCreationTask { + Q_OBJECT + public: + VanillaCreationTask(BaseVersionPtr version) : InstanceCreationTask(), m_version(std::move(version)) {} + VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version); + + bool createInstance() override; + + private: + // Version to update to / create of the instance. + BaseVersionPtr m_version; + + bool m_using_loader = false; + QString m_loader; + BaseVersionPtr m_loader_version; +}; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 9aca686f..66e80f4a 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -51,7 +51,6 @@ ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed) { - FS::ensureFolderPathExists(m_dir.absolutePath()); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE }; } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 95bd5648..b2356309 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -14,6 +14,8 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this) { + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp new file mode 100644 index 00000000..796eb69d --- /dev/null +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "TexturePack.h" + +#include <QDebug> +#include <QMap> +#include <QRegularExpression> + +#include "minecraft/mod/tasks/LocalTexturePackParseTask.h" + +void TexturePack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void TexturePack::setImage(QImage new_image) +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + QPixmapCache::remove(m_pack_image_cache_key.key); + + m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + m_pack_image_cache_key.was_ever_used = true; +} + +QPixmap TexturePack::image(QSize size) +{ + QPixmap cached_image; + if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + return {}; + + // Imaged got evicted from the cache. Re-process it and retry. + TexturePackUtils::process(*this); + return image(size); +} diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h new file mode 100644 index 00000000..6aa5e18e --- /dev/null +++ b/launcher/minecraft/mod/TexturePack.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "Resource.h" + +#include <QImage> +#include <QMutex> +#include <QPixmap> +#include <QPixmapCache> + +class Version; + +class TexturePack : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + TexturePack(QObject* parent = nullptr) : Resource(parent) {} + TexturePack(QFileInfo file_info) : Resource(file_info) {} + + /** Gets the description of the texture pack. */ + [[nodiscard]] QString description() const { return m_description; } + + /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap image(QSize size); + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. */ + void setImage(QImage new_image); + + protected: + mutable QMutex m_data_lock; + + /** The texture pack's description, as defined in the pack.txt file. + */ + QString m_description; + + /** The texture pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + } m_pack_image_cache_key; +}; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 2c7c945b..561f6202 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -1,38 +1,52 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, version 3. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see <https://www.gnu.org/licenses/>. -* -* This file incorporates work covered by the following copyright and -* permission notice: -* -* Copyright 2013-2021 MultiMC Contributors -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "TexturePackFolderModel.h" +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalTexturePackParseTask.h" + TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} + +Task* TexturePackFolderModel::createUpdateTask() +{ + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new TexturePack(entry); }); +} + +Task* TexturePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource)); +} diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 69e98661..261f83b4 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ResourceFolderModel.h" @@ -8,4 +44,6 @@ class TexturePackFolderModel : public ResourceFolderModel public: explicit TexturePackFolderModel(const QString &dir); + [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Task* createParseTask(Resource&) override; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 8a6e54d8..a694e7b2 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -1,17 +1,17 @@ #include "LocalModParseTask.h" +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> +#include <toml++/toml.h> +#include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> -#include <QJsonArray> #include <QJsonValue> #include <QString> -#include <quazip/quazip.h> -#include <quazip/quazipfile.h> -#include <toml.h> +#include "FileSystem.h" #include "Json.h" #include "settings/INIFile.h" -#include "FileSystem.h" namespace { @@ -22,8 +22,7 @@ namespace { // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc ModDetails ReadMCModInfo(QByteArray contents) { - auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails - { + auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails { if (!arr.at(0).isObject()) { return {}; } @@ -32,16 +31,14 @@ ModDetails ReadMCModInfo(QByteArray contents) 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") { + if (name != "Example Mod") { details.name = name; } details.version = firstObj.value("version").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); - if(!homeurl.isEmpty()) - { + if (!homeurl.isEmpty()) { // fix up url. - if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) - { + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) { homeurl.prepend("http://"); } } @@ -53,8 +50,7 @@ ModDetails ReadMCModInfo(QByteArray contents) authors = firstObj.value("authors").toArray(); } - for (auto author: authors) - { + for (auto author : authors) { details.authors.append(author.toString()); } return details; @@ -62,14 +58,11 @@ ModDetails ReadMCModInfo(QByteArray contents) QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); // this is the very old format that had just the array - if (jsonDoc.isArray()) - { + if (jsonDoc.isArray()) { return getInfoFromArray(jsonDoc.array()); - } - else if (jsonDoc.isObject()) - { + } else if (jsonDoc.isObject()) { auto val = jsonDoc.object().value("modinfoversion"); - if(val.isUndefined()) { + if (val.isUndefined()) { val = jsonDoc.object().value("modListVersion"); } @@ -79,18 +72,16 @@ ModDetails ReadMCModInfo(QByteArray contents) if (version < 0) version = Json::ensureString(val, "").toInt(); - if (version != 2) - { + if (version != 2) { qCritical() << "BAD stuff happened to mod json:"; qCritical() << contents; return {}; } auto arrVal = jsonDoc.object().value("modlist"); - if(arrVal.isUndefined()) { + if (arrVal.isUndefined()) { arrVal = jsonDoc.object().value("modList"); } - if (arrVal.isArray()) - { + if (arrVal.isArray()) { return getInfoFromArray(arrVal.toArray()); } } @@ -102,109 +93,76 @@ ModDetails ReadMCModTOML(QByteArray contents) { ModDetails details; - char errbuf[200]; - // top-level table - toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf)); - - if(!tomlData) - { + toml::table tomlData; +#if TOML_EXCEPTIONS + try { + tomlData = toml::parse(contents.toStdString()); + } catch (const toml::parse_error& err) { + return {}; + } +#else + tomlData = toml::parse(contents.toStdString()); + if (!tomlData) { return {}; } +#endif // array defined by [[mods]] - toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); - if(!tomlModsArr) - { + auto tomlModsArr = tomlData["mods"].as_array(); + if (!tomlModsArr) { qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; return {}; } // 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); - if(!tomlModsTable0) - { + auto tomlModsTable0 = tomlModsArr->get(0); + if (!tomlModsTable0) { qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; return {}; } + auto modsTable = tomlModsTable0->as_table(); + if (!tomlModsTable0) { + qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; + return {}; + } // 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); + if (auto modIdDatum = (*modsTable)["modId"].as_string()) { + details.mod_id = QString::fromStdString(modIdDatum->get()); } - toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version"); - if(versionDatum.ok) - { - details.version = versionDatum.u.s; - free(versionDatum.u.s); + if (auto versionDatum = (*modsTable)["version"].as_string()) { + details.version = QString::fromStdString(versionDatum->get()); } - toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); - if(displayNameDatum.ok) - { - details.name = displayNameDatum.u.s; - free(displayNameDatum.u.s); + if (auto displayNameDatum = (*modsTable)["displayName"].as_string()) { + details.name = QString::fromStdString(displayNameDatum->get()); } - toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description"); - if(descriptionDatum.ok) - { - details.description = descriptionDatum.u.s; - free(descriptionDatum.u.s); + if (auto descriptionDatum = (*modsTable)["description"].as_string()) { + details.description = QString::fromStdString(descriptionDatum->get()); } // 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 (auto authorsDatum = tomlData["authors"].as_string()) { + authors = QString::fromStdString(authorsDatum->get()); + } else if (auto authorsDatum = (*modsTable)["authors"].as_string()) { + authors = QString::fromStdString(authorsDatum->get()); } - if(!authors.isEmpty()) - { + if (!authors.isEmpty()) { details.authors.append(authors); } - 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 (auto homeurlDatum = tomlData["displayURL"].as_string()) { + homeurl = QString::fromStdString(homeurlDatum->get()); + } else if (auto homeurlDatum = (*modsTable)["displayURL"].as_string()) { + homeurl = QString::fromStdString(homeurlDatum->get()); } - if(!homeurl.isEmpty()) - { - // fix up url. - if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) - { - homeurl.prepend("http://"); - } + // fix up url. + if (!homeurl.isEmpty() && !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; } @@ -224,25 +182,20 @@ ModDetails ReadFabricModInfo(QByteArray contents) details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; details.description = object.value("description").toString(); - if (schemaVersion >= 1) - { + if (schemaVersion >= 1) { QJsonArray authors = object.value("authors").toArray(); - for (auto author: authors) - { - if(author.isObject()) { + for (auto author : authors) { + if (author.isObject()) { details.authors.append(author.toObject().value("name").toString()); - } - else { + } else { details.authors.append(author.toString()); } } - if (object.contains("contact")) - { + if (object.contains("contact")) { QJsonObject contact = object.value("contact").toObject(); - if (contact.contains("homepage")) - { + if (contact.contains("homepage")) { details.homeurl = contact.value("homepage").toString(); } } @@ -261,8 +214,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents) ModDetails details; // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md - if (schemaVersion == 1) - { + if (schemaVersion == 1) { auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); @@ -280,8 +232,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents) auto modContact = Json::ensureObject(modMetadata.value("contact")); - if (modContact.contains("homepage")) - { + if (modContact.contains("homepage")) { details.homeurl = Json::requireString(modContact.value("homepage")); } } @@ -314,21 +265,17 @@ ModDetails ReadLiteModInfo(QByteArray contents) QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); - if (object.contains("name")) - { + if (object.contains("name")) { details.mod_id = details.name = object.value("name").toString(); } - if (object.contains("version")) - { + if (object.contains("version")) { details.version = object.value("version").toString(""); - } - else - { + } else { details.version = object.value("revision").toString(""); } details.mcversion = object.value("mcversion").toString(); auto author = object.value("author").toString(); - if(!author.isEmpty()) { + if (!author.isEmpty()) { details.authors.append(author); } details.description = object.value("description").toString(); @@ -336,14 +283,10 @@ ModDetails ReadLiteModInfo(QByteArray contents) return details; } -} +} // namespace -LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile): - Task(nullptr, false), - m_token(token), - m_type(type), - m_modFile(modFile), - m_result(new Result()) +LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) + : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} void LocalModParseTask::processAsZip() @@ -354,10 +297,8 @@ void LocalModParseTask::processAsZip() QuaZipFile file(&zip); - if (zip.setCurrentFile("META-INF/mods.toml")) - { - if (!file.open(QIODevice::ReadOnly)) - { + if (zip.setCurrentFile("META-INF/mods.toml")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -366,12 +307,9 @@ void LocalModParseTask::processAsZip() file.close(); // to replace ${file.jarVersion} with the actual version, as needed - if (m_result->details.version == "${file.jarVersion}") - { - if (zip.setCurrentFile("META-INF/MANIFEST.MF")) - { - if (!file.open(QIODevice::ReadOnly)) - { + if (m_result->details.version == "${file.jarVersion}") { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -379,10 +317,8 @@ void LocalModParseTask::processAsZip() // 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: ")) - { + for (auto& line : manifestLines) { + if (QString(line).startsWith("Implementation-Version: ")) { manifestVersion = QString(line).remove("Implementation-Version: "); break; } @@ -390,8 +326,7 @@ void LocalModParseTask::processAsZip() // 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 == "") - { + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") { manifestVersion = "NONE"; } @@ -403,11 +338,8 @@ void LocalModParseTask::processAsZip() zip.close(); return; - } - else if (zip.setCurrentFile("mcmod.info")) - { - if (!file.open(QIODevice::ReadOnly)) - { + } else if (zip.setCurrentFile("mcmod.info")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -416,11 +348,8 @@ void LocalModParseTask::processAsZip() file.close(); zip.close(); return; - } - else if (zip.setCurrentFile("quilt.mod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { + } else if (zip.setCurrentFile("quilt.mod.json")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -429,11 +358,8 @@ void LocalModParseTask::processAsZip() file.close(); zip.close(); return; - } - else if (zip.setCurrentFile("fabric.mod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { + } else if (zip.setCurrentFile("fabric.mod.json")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -442,11 +368,8 @@ void LocalModParseTask::processAsZip() file.close(); zip.close(); return; - } - else if (zip.setCurrentFile("forgeversion.properties")) - { - if (!file.open(QIODevice::ReadOnly)) - { + } else if (zip.setCurrentFile("forgeversion.properties")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -463,8 +386,7 @@ void LocalModParseTask::processAsZip() void LocalModParseTask::processAsFolder() { QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); - if (mcmod_info.isFile()) - { + if (mcmod_info.isFile()) { QFile mcmod(mcmod_info.filePath()); if (!mcmod.open(QIODevice::ReadOnly)) return; @@ -483,10 +405,8 @@ void LocalModParseTask::processAsLitemod() QuaZipFile file(&zip); - if (zip.setCurrentFile("litemod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { + if (zip.setCurrentFile("litemod.json")) { + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; } @@ -505,8 +425,7 @@ bool LocalModParseTask::abort() void LocalModParseTask::executeTask() { - switch(m_type) - { + switch (m_type) { case ResourceType::ZIPFILE: processAsZip(); break; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp new file mode 100644 index 00000000..bf1e308f --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "LocalTexturePackParseTask.h" + +#include "FileSystem.h" + +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> + +#include <QCryptographicHash> + +namespace TexturePackUtils { + +bool process(TexturePack& pack) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + TexturePackUtils::processFolder(pack); + return true; + case ResourceType::ZIPFILE: + TexturePackUtils::processZIP(pack); + return true; + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + +void processFolder(TexturePack& pack) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.txt")); + if (mcmeta_file_info.isFile()) { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return; + + auto data = mcmeta_file.readAll(); + + TexturePackUtils::processPackTXT(pack, std::move(data)); + + mcmeta_file.close(); + } + + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.isFile()) { + QFile mcmeta_file(image_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return; + + auto data = mcmeta_file.readAll(); + + TexturePackUtils::processPackPNG(pack, std::move(data)); + + mcmeta_file.close(); + } +} + +void processZIP(TexturePack& pack) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("pack.txt")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return; + } + + auto data = file.readAll(); + + TexturePackUtils::processPackTXT(pack, std::move(data)); + + file.close(); + } + + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return; + } + + auto data = file.readAll(); + + TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + } + + zip.close(); +} + +void processPackTXT(TexturePack& pack, QByteArray&& raw_data) +{ + pack.setDescription(QString(raw_data)); +} + +void processPackPNG(TexturePack& pack, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + pack.setImage(img); + } else { + qWarning() << "Failed to parse pack.png."; + } +} +} // namespace TexturePackUtils + +LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) + : Task(nullptr, false), m_token(token), m_texture_pack(rp) +{} + +bool LocalTexturePackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalTexturePackParseTask::executeTask() +{ + Q_ASSERT(m_texture_pack.valid()); + + if (!TexturePackUtils::process(m_texture_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h new file mode 100644 index 00000000..cb0e404a --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/TexturePack.h" + +#include "tasks/Task.h" + +namespace TexturePackUtils { +bool process(TexturePack& pack); + +void processZIP(TexturePack& pack); +void processFolder(TexturePack& pack); + +void processPackTXT(TexturePack& pack, QByteArray&& raw_data); +void processPackPNG(TexturePack& pack, QByteArray&& raw_data); +} // namespace TexturePackUtils + +class LocalTexturePackParseTask : public Task { + Q_OBJECT + public: + LocalTexturePackParseTask(int token, TexturePack& rp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + TexturePack& m_texture_pack; + + bool m_aborted = false; +}; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 70a35395..a553eafd 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -90,6 +90,7 @@ void PackInstallTask::executeTask() QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + QObject::connect(netJob, &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); } void PackInstallTask::onDownloadSucceeded() @@ -169,6 +170,12 @@ void PackInstallTask::onDownloadFailed(QString reason) emitFailed(reason); } +void PackInstallTask::onDownloadAborted() +{ + jobPtr.reset(); + emitAborted(); +} + void PackInstallTask::deleteExistingFiles() { setStatus(tr("Deleting existing files...")); @@ -675,6 +682,11 @@ void PackInstallTask::installConfigs() abortable = true; setProgress(current, total); }); + connect(jobPtr.get(), &NetJob::aborted, [&]{ + abortable = false; + jobPtr.reset(); + emitAborted(); + }); jobPtr->start(); } @@ -831,6 +843,12 @@ void PackInstallTask::downloadMods() abortable = true; setProgress(current, total); }); + connect(jobPtr.get(), &NetJob::aborted, [&] + { + abortable = false; + jobPtr.reset(); + emitAborted(); + }); jobPtr->start(); } @@ -1005,7 +1023,7 @@ void PackInstallTask::install() components->saveNow(); - instance.setName(m_instName); + instance.setName(name()); instance.setIconKey(m_instIcon); instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); instanceSettings->resumeSave(); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index a7124d59..ed4436f0 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -93,6 +93,7 @@ protected: private slots: void onDownloadSucceeded(); void onDownloadFailed(QString reason); + void onDownloadAborted(); void onModsDownloaded(); void onModsExtracted(); diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 9c74918b..4d71da21 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -183,3 +183,26 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> return netJob; } + +auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob* +{ + auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray files_arr; + for (auto& fileId : fileIds) { + files_arr.append(fileId); + } + + body_obj["fileIds"] = files_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); + QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + + return netJob; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 4eac0664..4c6ca64c 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -12,6 +12,7 @@ class FlameAPI : public NetworkModAPI { auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; + auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*; private: inline auto getSortFieldInt(QString sortString) const -> int diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp new file mode 100644 index 00000000..48ac02e0 --- /dev/null +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -0,0 +1,457 @@ +#include "FlameInstanceCreationTask.h" + +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/PackManifest.h" + +#include "Application.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "Json.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/helpers/OverrideUtils.h" + +#include "settings/INISettingsObject.h" + +#include "ui/dialogs/BlockedModsDialog.h" +#include "ui/dialogs/CustomMessageBox.h" + +const static QMap<QString, QString> forgemap = { { "1.2.5", "3.4.9.171" }, + { "1.4.2", "6.0.1.355" }, + { "1.4.7", "6.6.2.534" }, + { "1.5.2", "7.8.1.737" } }; + +static const FlameAPI api; + +bool FlameCreationTask::abort() +{ + if (!canAbort()) + return false; + + m_abort = true; + if (m_process_update_file_info_job) + m_process_update_file_info_job->abort(); + if (m_files_job) + m_files_job->abort(); + if (m_mod_id_resolver) + m_mod_id_resolver->abort(); + + return Task::abort(); +} + +bool FlameCreationTask::updateInstance() +{ + auto instance_list = APPLICATION->instances(); + + // FIXME: How to handle situations when there's more than one install already for a given modpack? + auto inst = instance_list->getInstanceByManagedName(originalName()); + + if (!inst) { + inst = instance_list->getInstanceById(originalName()); + + if (!inst) + return false; + } + + QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); + + try { + Flame::loadManifest(m_pack, index_path); + } catch (const JSONValidationError& e) { + setError(tr("Could not understand pack manifest:\n") + e.cause()); + return false; + } + + auto version_id = inst->getManagedPackVersionName(); + auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; + + auto info = CustomMessageBox::selectable( + m_parent, tr("Similar modpack was found!"), + tr("One or more of your instances are from this same modpack%1. Do you want to create a " + "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " + "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") + .arg(version_str), QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); + info->setButtonText(QMessageBox::Ok, tr("Update existing instance")); + info->setButtonText(QMessageBox::Abort, tr("Create new instance")); + info->setButtonText(QMessageBox::Reset, tr("Cancel")); + + info->exec(); + + if (info->clickedButton() == info->button(QMessageBox::Abort)) + return false; + + if (info->clickedButton() == info->button(QMessageBox::Reset)) { + m_abort = true; + return false; + } + + QDir old_inst_dir(inst->instanceRoot()); + + QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "flame")); + QString old_index_path(FS::PathCombine(old_index_folder, "manifest.json")); + + QFileInfo old_index_file(old_index_path); + if (old_index_file.exists()) { + Flame::Manifest old_pack; + Flame::loadManifest(old_pack, old_index_path); + + auto& old_files = old_pack.files; + + auto& files = m_pack.files; + + // Remove repeated files, we don't need to download them! + auto files_iterator = files.begin(); + while (files_iterator != files.end()) { + auto const& file = files_iterator; + + auto old_file = old_files.find(file.key()); + if (old_file != old_files.end()) { + // We found a match, but is it a different version? + if (old_file->fileId == file->fileId) { + qDebug() << "Removed file at" << file->targetFolder << "with id" << file->fileId << "from list of downloads"; + + old_files.remove(file.key()); + files_iterator = files.erase(files_iterator); + } + } + + files_iterator++; + } + + QDir old_minecraft_dir(inst->gameRoot()); + + // We will remove all the previous overrides, to prevent duplicate files! + // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? + // FIXME: We may want to do something about disabled mods. + auto old_overrides = Override::readOverrides("overrides", old_index_folder); + for (const auto& entry : old_overrides) { + if (entry.isEmpty()) + continue; + qDebug() << "Scheduling" << entry << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + } + + // Remove remaining old files (we need to do an API request to know which ids are which files...) + QStringList fileIds; + + for (auto& file : old_files) { + fileIds.append(QString::number(file.fileId)); + } + + auto* raw_response = new QByteArray; + auto job = api.getFiles(fileIds, raw_response); + + QEventLoop loop; + + connect(job, &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { + // Parse the API response + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame files task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *raw_response; + return; + } + + try { + QJsonArray entries; + if (fileIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + + Flame::File file; + // We don't care about blocked mods, we just need local data to delete the file + file.parseFromObject(entry_obj, false); + + auto id = Json::requireInteger(entry_obj, "id"); + old_files.insert(id, file); + } + } catch (Json::JsonException& e) { + qCritical() << e.cause() << e.what(); + } + + // Delete the files + for (auto& file : old_files) { + if (file.fileName.isEmpty() || file.targetFolder.isEmpty()) + continue; + + QString relative_path(FS::PathCombine(file.targetFolder, file.fileName)); + qDebug() << "Scheduling" << relative_path << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); + } + }); + connect(job, &NetJob::finished, &loop, &QEventLoop::quit); + + m_process_update_file_info_job = job; + job->start(); + + loop.exec(); + + m_process_update_file_info_job = nullptr; + } else { + // We don't have an old index file, so we may duplicate stuff! + auto dialog = CustomMessageBox::selectable(m_parent, + tr("No index file."), + tr("We couldn't find a suitable index file for the older version. This may cause some of the files to be duplicated. Do you want to continue?"), + QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); + + if (dialog->exec() == QDialog::DialogCode::Rejected) { + m_abort = true; + return false; + } + } + + setOverride(true); + qDebug() << "Will override instance!"; + + m_instance = inst; + + // We let it go through the createInstance() stage, just with a couple modifications for updating + return false; +} + +bool FlameCreationTask::createInstance() +{ + QEventLoop loop; + + QString parent_folder(FS::PathCombine(m_stagingPath, "flame")); + + try { + QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); + if (!m_pack.is_loaded) + Flame::loadManifest(m_pack, index_path); + + // Keep index file in case we need it some other time (like when changing versions) + QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); + FS::ensureFilePathExists(new_index_place); + QFile::rename(index_path, new_index_place); + + } catch (const JSONValidationError& e) { + setError(tr("Could not understand pack manifest:\n") + e.cause()); + return false; + } + + if (!m_pack.overrides.isEmpty()) { + QString overridePath = FS::PathCombine(m_stagingPath, m_pack.overrides); + if (QFile::exists(overridePath)) { + // Create a list of overrides in "overrides.txt" inside flame/ + Override::createOverrides("overrides", parent_folder, overridePath); + + QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); + if (!QFile::rename(overridePath, mcPath)) { + setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); + return false; + } + } else { + logWarning( + tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(m_pack.overrides)); + } + } + + QString forgeVersion; + QString fabricVersion; + // TODO: is Quilt relevant here? + for (auto& loader : m_pack.minecraft.modLoaders) { + auto id = loader.id; + if (id.startsWith("forge-")) { + id.remove("forge-"); + forgeVersion = id; + continue; + } + if (id.startsWith("fabric-")) { + id.remove("fabric-"); + fabricVersion = id; + continue; + } + logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared<INISettingsObject>(configPath); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto mcVersion = m_pack.minecraft.version; + + // Hack to correct some 'special sauce'... + if (mcVersion.endsWith('.')) { + mcVersion.remove(QRegularExpression("[.]+$")); + logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", mcVersion, true); + if (!forgeVersion.isEmpty()) { + // FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata. + if (forgeVersion == "recommended") { + if (forgemap.contains(mcVersion)) { + forgeVersion = forgemap[mcVersion]; + } else { + logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion)); + } + } + components->setComponentVersion("net.minecraftforge", forgeVersion); + } + if (!fabricVersion.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); + + if (m_instIcon != "default") { + instance.setIconKey(m_instIcon); + } else { + if (m_pack.name.contains("Direwolf20")) { + instance.setIconKey("steve"); + } else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast")) { + instance.setIconKey("ftb_logo"); + } else { + instance.setIconKey("flame"); + } + } + + QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); + QFileInfo jarmodsInfo(jarmodsPath); + if (jarmodsInfo.isDir()) { + // install all the jar mods + qDebug() << "Found jarmods:"; + QDir jarmodsDir(jarmodsPath); + QStringList jarMods; + for (const auto& info : jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { + qDebug() << info.fileName(); + jarMods.push_back(info.absoluteFilePath()); + } + auto profile = instance.getPackProfile(); + profile->installJarMods(jarMods); + // nuke the original files + FS::deletePath(jarmodsPath); + } + + instance.setManagedPack("flame", {}, m_pack.name, {}, m_pack.version); + instance.setName(name()); + + m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack); + connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); + connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) { + m_mod_id_resolver.reset(); + setError(tr("Unable to resolve mod IDs:\n") + reason); + }); + connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); + connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); + + m_mod_id_resolver->start(); + + loop.exec(); + + bool did_succeed = getError().isEmpty(); + + // Update information of the already installed instance, if any. + if (m_instance && did_succeed) { + setAbortable(false); + auto inst = m_instance.value(); + + // Only change the name if it didn't use a custom name, so that the previous custom name + // is preserved, but if we're using the original one, we update the version string. + // NOTE: This needs to come before the copyManagedPack call! + if (inst->name().contains(inst->getManagedPackVersionName())) { + if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) + inst->setName(instance.name()); + } + + inst->copyManagedPack(instance); + } + + return did_succeed; +} + +void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) +{ + auto results = m_mod_id_resolver->getResults(); + + // first check for blocked mods + QString text; + QList<QUrl> urls; + auto anyBlocked = false; + for (const auto& result : results.files.values()) { + if (!result.resolved || result.url.isEmpty()) { + text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl); + urls.append(QUrl(result.websiteUrl)); + anyBlocked = true; + } + } + if (anyBlocked) { + qWarning() << "Blocked mods found, displaying mod list"; + + auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"), + tr("The following mods were blocked on third party launchers.<br/>" + "You will need to manually download them and add them to the modpack"), + text, + urls); + message_dialog->setModal(true); + + if (message_dialog->exec()) { + setupDownloadJob(loop); + } else { + m_mod_id_resolver.reset(); + setError("Canceled"); + loop.quit(); + } + } else { + setupDownloadJob(loop); + } +} + +void FlameCreationTask::setupDownloadJob(QEventLoop& loop) +{ + m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); + for (const auto& result : m_mod_id_resolver->getResults().files) { + QString filename = result.fileName; + if (!result.required) { + filename += ".disabled"; + } + + auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + auto path = FS::PathCombine(m_stagingPath, relpath); + + switch (result.type) { + case Flame::File::Type::Folder: { + logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); + // fall-through intentional, we treat these as plain old mods and dump them wherever. + } + case Flame::File::Type::SingleFile: + case Flame::File::Type::Mod: { + if (!result.url.isEmpty()) { + qDebug() << "Will download" << result.url << "to" << path; + auto dl = Net::Download::makeFile(result.url, path); + m_files_job->addNetAction(dl); + } + break; + } + case Flame::File::Type::Modpack: + logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath)); + break; + case Flame::File::Type::Cmod2: + case Flame::File::Type::Ctoc: + case Flame::File::Type::Unknown: + logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); + break; + } + } + + m_mod_id_resolver.reset(); + connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { + m_files_job.reset(); + }); + connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { + m_files_job.reset(); + setError(reason); + }); + connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); }); + connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); + + setStatus(tr("Downloading mods...")); + m_files_job->start(); +} diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h new file mode 100644 index 00000000..ded0e2ce --- /dev/null +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -0,0 +1,44 @@ +#pragma once + +#include "InstanceCreationTask.h" + +#include <optional> + +#include "minecraft/MinecraftInstance.h" + +#include "modplatform/flame/FileResolvingTask.h" + +#include "net/NetJob.h" + +class FlameCreationTask final : public InstanceCreationTask { + Q_OBJECT + + public: + FlameCreationTask(const QString& staging_path, SettingsObjectPtr global_settings, QWidget* parent) + : InstanceCreationTask(), m_parent(parent) + { + setStagingPath(staging_path); + setParentSettings(global_settings); + } + + bool abort() override; + + bool updateInstance() override; + bool createInstance() override; + + private slots: + void idResolverSucceeded(QEventLoop&); + void setupDownloadJob(QEventLoop&); + + private: + QWidget* m_parent = nullptr; + + shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver; + Flame::Manifest m_pack; + + // Handle to allow aborting + NetJob* m_process_update_file_info_job = nullptr; + NetJob::Ptr m_files_job = nullptr; + + std::optional<InstancePtr> m_instance; +}; diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 12a4b990..22008297 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -29,21 +29,29 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) } } -static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest) +static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) { auto mc = Json::requireObject(manifest, "minecraft"); - loadMinecraftV1(m.minecraft, mc); - m.name = Json::ensureString(manifest, QString("name"), "Unnamed"); - m.version = Json::ensureString(manifest, QString("version"), QString()); - m.author = Json::ensureString(manifest, QString("author"), "Anonymous"); + + loadMinecraftV1(pack.minecraft, mc); + + pack.name = Json::ensureString(manifest, QString("name"), "Unnamed"); + pack.version = Json::ensureString(manifest, QString("version"), QString()); + pack.author = Json::ensureString(manifest, QString("author"), "Anonymous"); + auto arr = Json::ensureArray(manifest, "files", QJsonArray()); - for (QJsonValueRef item : arr) { + for (auto item : arr) { auto obj = Json::requireObject(item); + Flame::File file; loadFileV1(file, obj); - m.files.insert(file.fileId,file); + + pack.files.insert(file.fileId,file); } - m.overrides = Json::ensureString(manifest, "overrides", "overrides"); + + pack.overrides = Json::ensureString(manifest, "overrides", "overrides"); + + pack.is_loaded = true; } void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) @@ -61,7 +69,7 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) loadManifestV1(m, obj); } -bool Flame::File::parseFromObject(const QJsonObject& obj) +bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked) { fileName = Json::requireString(obj, "fileName"); // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience @@ -91,7 +99,7 @@ bool Flame::File::parseFromObject(const QJsonObject& obj) // may throw, if the project is blocked QString rawUrl = Json::ensureString(obj, "downloadUrl"); url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { + if (!url.isValid() && throw_on_blocked) { throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); } diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 677db1c3..0b7461d8 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -35,18 +35,18 @@ #pragma once -#include <QString> -#include <QVector> +#include <QJsonObject> #include <QMap> +#include <QString> #include <QUrl> -#include <QJsonObject> +#include <QVector> namespace Flame { struct File { // NOTE: throws JSONValidationError - bool parseFromObject(const QJsonObject& object); + bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true); int projectId = 0; int fileId = 0; @@ -97,6 +97,8 @@ struct Manifest //File id -> File QMap<int,Flame::File> files; QString overrides; + + bool is_loaded = false; }; void loadManifest(Flame::Manifest & m, const QString &filepath); diff --git a/launcher/modplatform/helpers/OverrideUtils.cpp b/launcher/modplatform/helpers/OverrideUtils.cpp new file mode 100644 index 00000000..65b5f760 --- /dev/null +++ b/launcher/modplatform/helpers/OverrideUtils.cpp @@ -0,0 +1,59 @@ +#include "OverrideUtils.h" + +#include <QDirIterator> + +#include "FileSystem.h" + +namespace Override { + +void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path) +{ + QString file_path(FS::PathCombine(parent_folder, name + ".txt")); + if (QFile::exists(file_path)) + QFile::remove(file_path); + + FS::ensureFilePathExists(file_path); + + QFile file(file_path); + file.open(QFile::WriteOnly); + + QDirIterator override_iterator(override_path, QDirIterator::Subdirectories); + while (override_iterator.hasNext()) { + auto override_file_path = override_iterator.next(); + QFileInfo info(override_file_path); + if (info.isFile()) { + // Absolute path with temp directory -> relative path + override_file_path = override_file_path.split(name).last().remove(0, 1); + + file.write(override_file_path.toUtf8()); + file.write("\n"); + } + } + + file.close(); +} + +QStringList readOverrides(const QString& name, const QString& parent_folder) +{ + QString file_path(FS::PathCombine(parent_folder, name + ".txt")); + + QFile file(file_path); + if (!file.exists()) + return {}; + + QStringList previous_overrides; + + file.open(QFile::ReadOnly); + + QString entry; + do { + entry = file.readLine(); + previous_overrides.append(entry.trimmed()); + } while (!entry.isEmpty()); + + file.close(); + + return previous_overrides; +} + +} // namespace Override diff --git a/launcher/modplatform/helpers/OverrideUtils.h b/launcher/modplatform/helpers/OverrideUtils.h new file mode 100644 index 00000000..536261a2 --- /dev/null +++ b/launcher/modplatform/helpers/OverrideUtils.h @@ -0,0 +1,20 @@ +#pragma once + +#include <QString> + +namespace Override { + +/** This creates a file in `parent_folder` that holds information about which + * overrides are in `override_path`. + * + * If there's already an existing such file, it will be ovewritten. + */ +void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path); + +/** This reads an existing overrides archive, returning a list of overrides. + * + * If there's no such file in `parent_folder`, it will return an empty list. + */ +QStringList readOverrides(const QString& name, const QString& parent_folder); + +} // namespace Override diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index 4da6a866..36aa60c7 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -59,6 +59,7 @@ void PackFetchTask::fetch() QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); + QObject::connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); jobPtr->start(); } @@ -98,6 +99,14 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch) delete data; }); + QObject::connect(job, &NetJob::aborted, this, [this, job, data]{ + emit aborted(); + job->deleteLater(); + + data->clear(); + delete data; + }); + job->start(); } } @@ -204,4 +213,9 @@ void PackFetchTask::fileDownloadFailed(QString reason) emit failed(reason); } +void PackFetchTask::fileDownloadAborted() +{ + emit aborted(); +} + } diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index f1667e90..8f3c4f3b 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -33,10 +33,12 @@ private: protected slots: void fileDownloadFinished(); void fileDownloadFailed(QString reason); + void fileDownloadAborted(); signals: void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void failed(QString reason); + void aborted(); void privateFileDownloadFinished(Modpack modpack); void privateFileDownloadFailed(QString reason, QString packCode); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 83e14969..209ad884 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -86,6 +86,7 @@ void PackInstallTask::downloadPack() connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); + connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); netJobContainer->start(); progress(1, 4); @@ -110,6 +111,11 @@ void PackInstallTask::onDownloadProgress(qint64 current, qint64 total) setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10)); } +void PackInstallTask::onDownloadAborted() +{ + emitAborted(); +} + void PackInstallTask::unzip() { progress(2, 4); @@ -228,7 +234,7 @@ void PackInstallTask::install() progress(4, 4); - instance.setName(m_instName); + instance.setName(name()); if(m_instIcon == "default") { m_instIcon = "ftb_logo"; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index da4c0da5..da791e06 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -38,6 +38,7 @@ private slots: void onDownloadSucceeded(); void onDownloadFailed(QString reason); void onDownloadProgress(qint64 current, qint64 total); + void onDownloadAborted(); void onUnzipFinished(); void onUnzipCanceled(); diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 3c15667c..97ce1dc6 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -65,9 +65,8 @@ bool PackInstallTask::abort() if (m_mod_id_resolver_task) aborted &= m_mod_id_resolver_task->abort(); - // FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet... if (aborted) - emitFailed(tr("Aborted")); + emitAborted(); return aborted; } @@ -335,7 +334,7 @@ void PackInstallTask::install() components->saveNow(); - instance.setName(m_instName); + instance.setName(name()); instance.setIconKey(m_instIcon); instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); instanceSettings->resumeSave(); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp new file mode 100644 index 00000000..ddeea224 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -0,0 +1,407 @@ +#include "ModrinthInstanceCreationTask.h" + +#include "Application.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "Json.h" + +#include "minecraft/PackProfile.h" + +#include "modplatform/helpers/OverrideUtils.h" + +#include "net/ChecksumValidator.h" + +#include "settings/INISettingsObject.h" + +#include "ui/dialogs/CustomMessageBox.h" + +#include <QAbstractButton> + +bool ModrinthCreationTask::abort() +{ + if (!canAbort()) + return false; + + m_abort = true; + if (m_files_job) + m_files_job->abort(); + return Task::abort(); +} + +bool ModrinthCreationTask::updateInstance() +{ + auto instance_list = APPLICATION->instances(); + + // FIXME: How to handle situations when there's more than one install already for a given modpack? + auto inst = instance_list->getInstanceByManagedName(originalName()); + + if (!inst) { + inst = instance_list->getInstanceById(originalName()); + + if (!inst) + return false; + } + + QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (!parseManifest(index_path, m_files, true, false)) + return false; + + auto version_name = inst->getManagedPackVersionName(); + auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; + + auto info = CustomMessageBox::selectable( + m_parent, tr("Similar modpack was found!"), + tr("One or more of your instances are from this same modpack%1. Do you want to create a " + "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " + "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") + .arg(version_str), + QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); + info->setButtonText(QMessageBox::Ok, tr("Create new instance")); + info->setButtonText(QMessageBox::Abort, tr("Update existing instance")); + info->setButtonText(QMessageBox::Reset, tr("Cancel")); + + info->exec(); + + if (info->clickedButton() == info->button(QMessageBox::Ok)) + return false; + + if (info->clickedButton() == info->button(QMessageBox::Reset)) { + m_abort = true; + return false; + } + + // Remove repeated files, we don't need to download them! + QDir old_inst_dir(inst->instanceRoot()); + + QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack")); + + QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json")); + QFileInfo old_index_file(old_index_path); + if (old_index_file.exists()) { + std::vector<Modrinth::File> old_files; + parseManifest(old_index_path, old_files, false, false); + + // Let's remove all duplicated, identical resources! + auto files_iterator = m_files.begin(); + begin: + while (files_iterator != m_files.end()) { + auto const& file = *files_iterator; + + auto old_files_iterator = old_files.begin(); + while (old_files_iterator != old_files.end()) { + auto const& old_file = *old_files_iterator; + + if (old_file.hash == file.hash) { + qDebug() << "Removed file at" << file.path << "from list of downloads"; + files_iterator = m_files.erase(files_iterator); + old_files_iterator = old_files.erase(old_files_iterator); + goto begin; // Sorry :c + } + + old_files_iterator++; + } + + files_iterator++; + } + + QDir old_minecraft_dir(inst->gameRoot()); + + // Some files were removed from the old version, and some will be downloaded in an updated version, + // so we're fine removing them! + if (!old_files.empty()) { + for (auto const& file : old_files) { + if (file.path.isEmpty()) + continue; + qDebug() << "Scheduling" << file.path << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path)); + } + } + + // We will remove all the previous overrides, to prevent duplicate files! + // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? + // FIXME: We may want to do something about disabled mods. + auto old_overrides = Override::readOverrides("overrides", old_index_folder); + for (const auto& entry : old_overrides) { + if (entry.isEmpty()) + continue; + qDebug() << "Scheduling" << entry << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + } + + auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); + for (const auto& entry : old_overrides) { + if (entry.isEmpty()) + continue; + qDebug() << "Scheduling" << entry << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + } + } else { + // We don't have an old index file, so we may duplicate stuff! + auto dialog = CustomMessageBox::selectable(m_parent, + tr("No index file."), + tr("We couldn't find a suitable index file for the older version. This may cause some of the files to be duplicated. Do you want to continue?"), + QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); + + if (dialog->exec() == QDialog::DialogCode::Rejected) { + m_abort = true; + return false; + } + } + + + setOverride(true); + qDebug() << "Will override instance!"; + + m_instance = inst; + + // We let it go through the createInstance() stage, just with a couple modifications for updating + return false; +} + +// https://docs.modrinth.com/docs/modpacks/format_definition/ +bool ModrinthCreationTask::createInstance() +{ + QEventLoop loop; + + QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack")); + + QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (m_files.empty() && !parseManifest(index_path, m_files, true, true)) + return false; + + // Keep index file in case we need it some other time (like when changing versions) + QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); + FS::ensureFilePathExists(new_index_place); + QFile::rename(index_path, new_index_place); + + auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); + + auto override_path = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(override_path)) { + // Create a list of overrides in "overrides.txt" inside mrpack/ + Override::createOverrides("overrides", parent_folder, override_path); + + // Apply the overrides + if (!QFile::rename(override_path, mcPath)) { + setError(tr("Could not rename the overrides folder:\n") + "overrides"); + return false; + } + } + + // Do client overrides + auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(client_override_path)) { + // Create a list of overrides in "client-overrides.txt" inside mrpack/ + Override::createOverrides("client-overrides", parent_folder, client_override_path); + + // Apply the overrides + if (!FS::overrideFolder(mcPath, client_override_path)) { + setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); + return false; + } + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared<INISettingsObject>(configPath); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + + if (!fabricVersion.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); + if (!quiltVersion.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion); + if (!forgeVersion.isEmpty()) + components->setComponentVersion("net.minecraftforge", forgeVersion); + + if (m_instIcon != "default") { + instance.setIconKey(m_instIcon); + } else { + instance.setIconKey("modrinth"); + } + + instance.setManagedPack("modrinth", getManagedPackID(), m_managed_name, m_managed_version_id, version()); + instance.setName(name()); + instance.saveNow(); + + m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); + + for (auto file : m_files) { + auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); + qDebug() << "Will try to download" << file.downloads.front() << "to" << path; + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_files_job->addNetAction(dl); + + if (!file.downloads.empty()) { + // FIXME: This really needs to be put into a ConcurrentTask of + // MultipleOptionsTask's , once those exist :) + auto param = dl.toWeakRef(); + connect(dl.get(), &NetAction::failed, [this, &file, path, param] { + auto ndl = Net::Download::makeFile(file.downloads.dequeue(), path); + ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_files_job->addNetAction(ndl); + if (auto shared = param.lock()) shared->succeeded(); + }); + } + } + + bool ended_well = false; + + connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); + connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) { + ended_well = false; + setError(reason); + }); + connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); + connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); }); + + setStatus(tr("Downloading mods...")); + m_files_job->start(); + + loop.exec(); + + // Update information of the already installed instance, if any. + if (m_instance && ended_well) { + setAbortable(false); + auto inst = m_instance.value(); + + // Only change the name if it didn't use a custom name, so that the previous custom name + // is preserved, but if we're using the original one, we update the version string. + // NOTE: This needs to come before the copyManagedPack call! + if (inst->name().contains(inst->getManagedPackVersionName())) { + if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) + inst->setName(instance.name()); + } + + inst->copyManagedPack(instance); + } + + return ended_well; +} + +bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<Modrinth::File>& files, bool set_managed_info, bool show_optional_dialog) +{ + try { + auto doc = Json::requireDocument(index_path); + auto obj = Json::requireObject(doc, "modrinth.index.json"); + int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); + if (formatVersion == 1) { + auto game = Json::requireString(obj, "game", "modrinth.index.json"); + if (game != "minecraft") { + throw JSONValidationError("Unknown game: " + game); + } + + if (set_managed_info) { + m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); + m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); + } + + auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); + bool had_optional = false; + for (const auto& modInfo : jsonFiles) { + Modrinth::File file; + file.path = Json::requireString(modInfo, "path"); + + auto env = Json::ensureObject(modInfo, "env"); + // 'env' field is optional + if (!env.isEmpty()) { + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") { + continue; + } else if (support == "optional") { + // TODO: Make a review dialog for choosing which ones the user wants! + if (!had_optional && show_optional_dialog) { + had_optional = true; + auto info = CustomMessageBox::selectable( + m_parent, tr("Optional mod detected!"), + tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), + QMessageBox::Information); + info->exec(); + } + + if (file.path.endsWith(".jar")) + file.path += ".disabled"; + } + } + + QJsonObject hashes = Json::requireObject(modInfo, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) { + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) { + throw JSONValidationError("No hash found for: " + file.path); + } + } + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode + // (as Modrinth seems to incorrectly handle spaces) + + auto download_arr = Json::ensureArray(modInfo, "downloads"); + for (auto download : download_arr) { + qWarning() << download.toString(); + bool is_last = download.toString() == download_arr.last().toString(); + + auto download_url = QUrl(download.toString()); + + if (!download_url.isValid()) { + qDebug() + << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(download_url.toString(), file.path); + if (is_last && file.downloads.isEmpty()) + throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + } else { + file.downloads.push_back(download_url); + } + } + + files.push_back(file); + } + + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { + QString name = it.key(); + if (name == "minecraft") { + minecraftVersion = Json::requireString(*it, "Minecraft version"); + } else if (name == "fabric-loader") { + fabricVersion = Json::requireString(*it, "Fabric Loader version"); + } else if (name == "quilt-loader") { + quiltVersion = Json::requireString(*it, "Quilt Loader version"); + } else if (name == "forge") { + forgeVersion = Json::requireString(*it, "Forge version"); + } else { + throw JSONValidationError("Unknown dependency type: " + name); + } + } + } else { + throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); + } + + } catch (const JSONValidationError& e) { + setError(tr("Could not understand pack index:\n") + e.cause()); + return false; + } + + return true; +} + +QString ModrinthCreationTask::getManagedPackID() const +{ + if (!m_source_url.isEmpty()) { + QRegularExpression regex(R"(data\/(.*)\/versions)"); + return regex.match(m_source_url).captured(1); + } + + return {}; +} diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h new file mode 100644 index 00000000..e459aadf --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -0,0 +1,44 @@ +#pragma once + +#include "InstanceCreationTask.h" + +#include <optional> + +#include "minecraft/MinecraftInstance.h" + +#include "modplatform/modrinth/ModrinthPackManifest.h" + +#include "net/NetJob.h" + +class ModrinthCreationTask final : public InstanceCreationTask { + Q_OBJECT + + public: + ModrinthCreationTask(QString staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString source_url = {}) + : InstanceCreationTask(), m_parent(parent), m_source_url(std::move(source_url)) + { + setStagingPath(staging_path); + setParentSettings(global_settings); + } + + bool abort() override; + + bool updateInstance() override; + bool createInstance() override; + + private: + bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_managed_info = true, bool show_optional_dialog = true); + QString getManagedPackID() const; + + private: + QWidget* m_parent = nullptr; + + QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; + QString m_managed_id, m_managed_version_id, m_managed_name; + QString m_source_url; + + std::vector<Modrinth::File> m_files; + NetJob::Ptr m_files_job; + + std::optional<InstancePtr> m_instance; +}; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index c3561093..b1fe963e 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -1,20 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, version 3. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ #include "Packwiz.h" @@ -22,9 +22,7 @@ #include <QDir> #include <QObject> -#include "toml.h" -#include "FileSystem.h" - +#include <toml++/toml.h> #include "minecraft/mod/Mod.h" #include "modplatform/ModIndex.h" @@ -44,7 +42,7 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin } } - if(should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)){ + if (should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)) { qCritical() << "Could not find a match for a valid metadata file!"; qCritical() << "File: " << normalized_fname; return {}; @@ -57,7 +55,7 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin // Helpers static inline auto indexFileName(QString const& mod_slug) -> QString { - if(mod_slug.endsWith(".pw.toml")) + if (mod_slug.endsWith(".pw.toml")) return mod_slug; return QString("%1.pw.toml").arg(mod_slug); } @@ -65,32 +63,28 @@ static inline auto indexFileName(QString const& mod_slug) -> QString static ModPlatform::ProviderCapabilities ProviderCaps; // Helper functions for extracting data from the TOML file -auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString +auto stringEntry(toml::table table, const std::string entry_name) -> QString { - toml_datum_t var = toml_string_in(parent, entry_name); - if (!var.ok) { - qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name); + auto node = table[entry_name]; + if (!node) { + qCritical() << QString::fromStdString("Failed to read str property '" + entry_name + "' in mod metadata."); return {}; } - QString tmp = var.u.s; - free(var.u.s); - - return tmp; + return QString::fromStdString(node.value_or("")); } -auto intEntry(toml_table_t* parent, const char* entry_name) -> int +auto intEntry(toml::table table, const std::string entry_name) -> int { - toml_datum_t var = toml_int_in(parent, entry_name); - if (!var.ok) { - qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name); + auto node = table[entry_name]; + if (!node) { + qCritical() << QString::fromStdString("Failed to read int property '" + entry_name + "' in mod metadata."); return {}; } - return var.u.i; + return node.value_or(0); } - auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; @@ -99,10 +93,9 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.name = mod_pack.name; mod.filename = mod_version.fileName; - if(mod_pack.provider == ModPlatform::Provider::FLAME){ + if (mod_pack.provider == ModPlatform::Provider::FLAME) { mod.mode = "metadata:curseforge"; - } - else { + } else { mod.mode = "url"; mod.url = mod_version.downloadUrl; } @@ -120,8 +113,8 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod { // Try getting metadata if it exists - Mod mod { getIndexForMod(index_dir, slug) }; - if(mod.isValid()) + Mod mod{ getIndexForMod(index_dir, slug) }; + if (mod.isValid()) return mod; qWarning() << QString("Tried to create mod metadata with a Mod without metadata!"); @@ -131,7 +124,7 @@ auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> void V1::updateModIndex(QDir& index_dir, Mod& mod) { - if(!mod.isValid()){ + if (!mod.isValid()) { qCritical() << QString("Tried to update metadata of an invalid mod!"); return; } @@ -150,7 +143,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) // TODO: We should do more stuff here, as the user is likely trying to // override a file. In this case, check versions and ask the user what // they want to do! - if (index_file.exists()) { index_file.remove(); } + if (index_file.exists()) { + index_file.remove(); + } if (!index_file.open(QIODevice::ReadWrite)) { qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); @@ -174,15 +169,15 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) in_stream << QString("\n[update]\n"); in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); - switch(mod.provider){ - case(ModPlatform::Provider::FLAME): - in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); - in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); - break; - case(ModPlatform::Provider::MODRINTH): - addToStream("mod-id", mod.mod_id().toString()); - addToStream("version", mod.version().toString()); - break; + switch (mod.provider) { + case (ModPlatform::Provider::FLAME): + in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); + in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); + break; + case (ModPlatform::Provider::MODRINTH): + addToStream("mod-id", mod.mod_id().toString()); + addToStream("version", mod.version().toString()); + break; } } @@ -230,27 +225,25 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod if (real_fname.isEmpty()) return {}; - QFile index_file(index_dir.absoluteFilePath(real_fname)); - - if (!index_file.open(QIODevice::ReadOnly)) { - qWarning() << QString("Failed to open mod metadata for %1").arg(slug); + toml::table table; +#if TOML_EXCEPTIONS + try { + table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); + } catch (const toml::parse_error& err) { + qWarning() << QString("Could not open file %1!").arg(normalized_fname); + qWarning() << "Reason: " << QString(err.what()); return {}; } - - toml_table_t* table = nullptr; - - // NOLINTNEXTLINE(modernize-avoid-c-arrays) - char errbuf[200]; - auto file_bytearray = index_file.readAll(); - table = toml_parse(file_bytearray.data(), errbuf, sizeof(errbuf)); - - index_file.close(); - +#else + table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); if (!table) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); - qWarning() << "Reason: " << QString(errbuf); + qWarning() << "Reason: " << QString(table.error().what()); return {}; } +#endif + + // index_file.close(); mod.slug = slug; @@ -261,45 +254,42 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod } { // [download] info - toml_table_t* download_table = toml_table_in(table, "download"); + auto download_table = table["download"].as_table(); if (!download_table) { qCritical() << QString("No [download] section found on mod metadata!"); return {}; } - mod.mode = stringEntry(download_table, "mode"); - mod.url = stringEntry(download_table, "url"); - mod.hash_format = stringEntry(download_table, "hash-format"); - mod.hash = stringEntry(download_table, "hash"); + mod.mode = stringEntry(*download_table, "mode"); + mod.url = stringEntry(*download_table, "url"); + mod.hash_format = stringEntry(*download_table, "hash-format"); + mod.hash = stringEntry(*download_table, "hash"); } - { // [update] info + { // [update] info using Provider = ModPlatform::Provider; - toml_table_t* update_table = toml_table_in(table, "update"); - if (!update_table) { + auto update_table = table["update"]; + if (!update_table || !update_table.is_table()) { qCritical() << QString("No [update] section found on mod metadata!"); return {}; } - toml_table_t* mod_provider_table = nullptr; - if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::FLAME)))) { + toml::table* mod_provider_table = nullptr; + if ((mod_provider_table = update_table[ProviderCaps.name(Provider::FLAME)].as_table())) { mod.provider = Provider::FLAME; - mod.file_id = intEntry(mod_provider_table, "file-id"); - mod.project_id = intEntry(mod_provider_table, "project-id"); - } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::MODRINTH)))) { + mod.file_id = intEntry(*mod_provider_table, "file-id"); + mod.project_id = intEntry(*mod_provider_table, "project-id"); + } else if ((mod_provider_table = update_table[ProviderCaps.name(Provider::MODRINTH)].as_table())) { mod.provider = Provider::MODRINTH; - mod.mod_id() = stringEntry(mod_provider_table, "mod-id"); - mod.version() = stringEntry(mod_provider_table, "version"); + mod.mod_id() = stringEntry(*mod_provider_table, "mod-id"); + mod.version() = stringEntry(*mod_provider_table, "version"); } else { qCritical() << QString("No mod provider on mod metadata!"); return {}; } - } - toml_free(table); - return mod; } diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index 9093b245..6438d9ef 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -133,7 +133,7 @@ void Technic::SingleZipPackInstallTask::extractFinished() 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); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion); } void Technic::SingleZipPackInstallTask::extractAborted() diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 89dbf4ca..19731b38 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -77,6 +77,7 @@ void Technic::SolderPackInstallTask::executeTask() auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } @@ -127,6 +128,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() 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); + connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } @@ -171,6 +173,12 @@ void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qin setProgress(current / 2, total); } +void Technic::SolderPackInstallTask::downloadAborted() +{ + emitAborted(); + m_filesNetJob.reset(); +} + void Technic::SolderPackInstallTask::extractFinished() { if (!m_extractFuture.result()) @@ -214,7 +222,7 @@ void Technic::SolderPackInstallTask::extractFinished() 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); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true); } void Technic::SolderPackInstallTask::extractAborted() diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index 117a7bd6..aa14ce88 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -61,6 +61,7 @@ namespace Technic void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); + void downloadAborted(); void extractFinished(); void extractAborted(); diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 2baf0188..3d607dca 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -68,7 +68,7 @@ class Task : public QObject, public QRunnable { virtual QStringList warnings() const; - virtual bool canAbort() const { return false; } + virtual bool canAbort() const { return m_can_abort; } auto getState() const -> State { return m_state; } @@ -96,6 +96,10 @@ class Task : public QObject, public QRunnable { void status(QString status); void stepStatus(QString status); + /** Emitted when the canAbort() status has changed. + */ + void abortStatusChanged(bool can_abort); + public slots: // QRunnable's interface void run() override { start(); } @@ -103,6 +107,8 @@ class Task : public QObject, public QRunnable { virtual void start(); virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; + void setAbortable(bool can_abort) { m_can_abort = can_abort; emit abortStatusChanged(can_abort); } + protected: virtual void executeTask() = 0; @@ -125,4 +131,8 @@ class Task : public QObject, public QRunnable { // TODO: Nuke in favor of QLoggingCategory bool m_show_debug = true; + + private: + // Change using setAbortStatus + bool m_can_abort = false; }; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 848b4d19..2f57de3a 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -86,6 +86,10 @@ struct Language else { result = locale.nativeLanguageName(); } + + if (result.isEmpty()) { + result = key; + } return result; } @@ -394,7 +398,7 @@ void TranslationsModel::reloadLocalFiles() return false; } } - return a.key < b.key; + return a.languageName().toLower() < b.languageName().toLower(); }); endInsertRows(); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 58b1ae80..5729b44d 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1656,6 +1656,10 @@ void MainWindow::runModalTask(Task *task) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } }); + connect(task, &Task::aborted, [this] + { + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); + }); ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(task); diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 675f8b15..d203795a 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -51,6 +51,7 @@ #include <QFileDialog> #include <QValidator> #include <QDialogButtonBox> +#include <utility> #include "ui/widgets/PageContainer.h" #include "ui/pages/modplatform/VanillaPage.h" @@ -180,10 +181,27 @@ NewInstanceDialog::~NewInstanceDialog() void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task) { creationTask.reset(task); + ui->instNameTextBox->setPlaceholderText(name); + importVersion.clear(); - if(!task) - { + if (!task) { + ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); + importIcon = false; + } + + auto allowOK = task && !instName().isEmpty(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, InstanceTask* task) +{ + creationTask.reset(task); + + ui->instNameTextBox->setPlaceholderText(name); + importVersion = std::move(version); + + if (!task) { ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); importIcon = false; } @@ -214,7 +232,11 @@ InstanceTask * NewInstanceDialog::extractTask() { InstanceTask * extracted = creationTask.get(); creationTask.release(); - extracted->setName(instName()); + + InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion); + inst_name.setName(ui->instNameTextBox->text().trimmed()); + extracted->setName(inst_name); + extracted->setGroup(instGroup()); extracted->setIcon(iconKey()); return extracted; diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h index a3c8cd1c..961f512e 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.h +++ b/launcher/ui/dialogs/NewInstanceDialog.h @@ -37,7 +37,6 @@ #include <QDialog> -#include "BaseVersion.h" #include "ui/pages/BasePageProvider.h" #include "InstanceTask.h" @@ -61,7 +60,8 @@ public: void updateDialogState(); - void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr); + void setSuggestedPack(const QString& name = QString(), InstanceTask * task = nullptr); + void setSuggestedPack(const QString& name, QString version, InstanceTask * task = nullptr); void setSuggestedIconFromFile(const QString &path, const QString &name); void setSuggestedIcon(const QString &key); @@ -95,5 +95,7 @@ private: QString importIconPath; QString importIconName; + QString importVersion; + void importIconNow(); }; diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 3c7f53d3..258a32e4 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -43,8 +43,7 @@ void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); - if (task->abort()) - QDialog::reject(); + task->abort(); } ProgressDialog::~ProgressDialog() @@ -81,7 +80,8 @@ int ProgressDialog::execWithTask(Task* task) connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus); connect(task, &Task::progress, this, &ProgressDialog::changeProgress); - connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); + connect(task, &Task::aborted, [this] { QDialog::reject(); }); + connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled); m_is_multi_step = task->isMultiStep(); if (!m_is_multi_step) { diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 645f7ef6..0d14f147 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -176,7 +176,7 @@ <item> <widget class="QLabel" name="metadataWarningLabel"> <property name="text"> - <string><html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some upcoming QoL features, such as mod updating!</span></p></body></html></string> + <string><html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html></string> </property> <property name="wordWrap"> <bool>true</bool> diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index fa219eda..69b836ca 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -39,6 +39,7 @@ #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/TexturePackFolderModel.h" +#include "minecraft/mod/TexturePack.h" class TexturePackPage : public ExternalResourcesPage { @@ -60,4 +61,15 @@ public: { return m_instance->traits().contains("texturepacks"); } + + public slots: + bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override + { + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& rp = static_cast<TexturePack&>(m_model->at(row)); + ui->frame->updateWithTexturePack(rp); + + return true; + } }; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 029e2be0..8961fadd 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -62,11 +62,7 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant } case Qt::DecorationRole: { if (m_logoMap.contains(pack.logoName)) { - auto icon = m_logoMap.value(pack.logoName); - // FIXME: This doesn't really belong here, but Qt doesn't offer a good way right now ;( - auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48)); - - return icon_scaled; + return m_logoMap.value(pack.logoName); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); // un-const-ify this @@ -175,7 +171,7 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac void ListModel::requestLogo(QString logo, QString url) { - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) { return; } diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 986caa77..4fce0242 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -60,6 +60,7 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); + connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index a026947f..99190f31 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -39,12 +39,12 @@ #include <QTabBar> #include "Application.h" +#include "Filter.h" +#include "Version.h" #include "meta/Index.h" #include "meta/VersionList.h" +#include "minecraft/VanillaInstanceCreationTask.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "Filter.h" -#include "InstanceCreationTask.h" -#include "Version.h" VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) @@ -217,11 +217,11 @@ void VanillaPage::suggestCurrent() // There isn't a selected version if the version list is empty if(ui->loaderVersionList->selectedVersion() == nullptr) - dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + dialog->setSuggestedPack(m_selectedVersion->descriptor(), new VanillaCreationTask(m_selectedVersion)); else { dialog->setSuggestedPack(m_selectedVersion->descriptor(), - new InstanceCreationTask(m_selectedVersion, m_selectedLoader, + new VanillaCreationTask(m_selectedVersion, m_selectedLoader, m_selectedLoaderVersion)); } dialog->setSuggestedIcon("default"); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 7901b90b..87544445 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -117,7 +117,7 @@ void AtlPage::suggestCurrent() } auto uiSupport = new AtlUserInteractionSupportImpl(this); - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion)); + dialog->setSuggestedPack(selected.name, selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion)); auto editedLogoName = selected.safeName; auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index 504d7f7b..8975d74e 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -127,7 +127,7 @@ void FtbPage::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this)); + dialog->setSuggestedPack(selected.name, selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this)); for(auto art : selected.art) { if(art.type == "square") { QString editedLogoName; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 6ffbd312..98ab8799 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -146,6 +146,7 @@ void Page::openedImpl() { connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully); connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed); + connect(ftbFetchTask.get(), &PackFetchTask::aborted, this, &Page::ftbPackDataDownloadAborted); connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully); connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed); @@ -176,7 +177,7 @@ void Page::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); + dialog->setSuggestedPack(selected.name, selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); QString editedLogoName; if(selected.logo.toLower().startsWith("ftb")) { @@ -220,7 +221,12 @@ void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList void Page::ftbPackDataDownloadFailed(QString reason) { - //TODO: Display the error + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); +} + +void Page::ftbPackDataDownloadAborted() +{ + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); } void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index 52db7d91..1de8b40a 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -95,6 +95,7 @@ private: private slots: void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks); void ftbPackDataDownloadFailed(QString reason); + void ftbPackDataDownloadAborted(); void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 614be434..fd7a3537 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -41,6 +41,7 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/widgets/ProjectItem.h" #include <QMessageBox> @@ -74,31 +75,40 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian } Modrinth::Modpack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::ToolTipRole) { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); - return edit; + switch (role) { + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - return pack.description; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.iconName)) { - auto icon = m_logoMap.value(pack.iconName); - // FIXME: This doesn't really belong here, but Qt doesn't offer a good way right now ;( - auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48)); - - return icon_scaled; + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.iconName)) + return m_logoMap.value(pack.iconName); + + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + default: + break; } return {}; @@ -217,7 +227,7 @@ void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, Logo void ModpackListModel::requestLogo(QString logo, QString url) { - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) { return; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index df29c0c3..cea6cdee 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -43,6 +43,8 @@ #include "InstanceImportTask.h" #include "Json.h" +#include "ui/widgets/ProjectItem.h" + #include <HoeDown.h> #include <QComboBox> @@ -70,6 +72,8 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } ModrinthPage::~ModrinthPage() @@ -294,7 +298,7 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url, this)); + dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index b8c1e00a..b15af244 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -271,11 +271,11 @@ void TechnicPage::selectVersion() { if (!current.isSolder) { - dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name, selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { - dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion)); + dialog->setSuggestedPack(current.name, selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion)); } } diff --git a/launcher/ui/themes/BrightTheme.cpp b/launcher/ui/themes/BrightTheme.cpp index b9188bdd..696ffdfb 100644 --- a/launcher/ui/themes/BrightTheme.cpp +++ b/launcher/ui/themes/BrightTheme.cpp @@ -1,5 +1,7 @@ #include "BrightTheme.h" +#include <QObject> + QString BrightTheme::id() { return "bright"; @@ -18,19 +20,19 @@ bool BrightTheme::hasColorScheme() QPalette BrightTheme::colorScheme() { QPalette brightPalette; - brightPalette.setColor(QPalette::Window, QColor(239,240,241)); - brightPalette.setColor(QPalette::WindowText, QColor(49,54,59)); - brightPalette.setColor(QPalette::Base, QColor(252,252,252)); - brightPalette.setColor(QPalette::AlternateBase, QColor(239,240,241)); - brightPalette.setColor(QPalette::ToolTipBase, QColor(49,54,59)); - brightPalette.setColor(QPalette::ToolTipText, QColor(239,240,241)); - brightPalette.setColor(QPalette::Text, QColor(49,54,59)); - brightPalette.setColor(QPalette::Button, QColor(239,240,241)); - brightPalette.setColor(QPalette::ButtonText, QColor(49,54,59)); + brightPalette.setColor(QPalette::Window, QColor(255,255,255)); + brightPalette.setColor(QPalette::WindowText, QColor(17,17,17)); + brightPalette.setColor(QPalette::Base, QColor(250,250,250)); + brightPalette.setColor(QPalette::AlternateBase, QColor(240,240,240)); + brightPalette.setColor(QPalette::ToolTipBase, QColor(17,17,17)); + brightPalette.setColor(QPalette::ToolTipText, QColor(255,255,255)); + brightPalette.setColor(QPalette::Text, Qt::black); + brightPalette.setColor(QPalette::Button, QColor(249,249,249)); + brightPalette.setColor(QPalette::ButtonText, Qt::black); brightPalette.setColor(QPalette::BrightText, Qt::red); - brightPalette.setColor(QPalette::Link, QColor(41, 128, 185)); - brightPalette.setColor(QPalette::Highlight, QColor(61, 174, 233)); - brightPalette.setColor(QPalette::HighlightedText, QColor(239,240,241)); + brightPalette.setColor(QPalette::Link, QColor(37,137,164)); + brightPalette.setColor(QPalette::Highlight, QColor(137,207,84)); + brightPalette.setColor(QPalette::HighlightedText, Qt::black); return fadeInactive(brightPalette, fadeAmount(), fadeColor()); } @@ -41,7 +43,7 @@ double BrightTheme::fadeAmount() QColor BrightTheme::fadeColor() { - return QColor(239,240,241); + return QColor(255,255,255); } bool BrightTheme::hasStyleSheet() diff --git a/launcher/ui/themes/DarkTheme.cpp b/launcher/ui/themes/DarkTheme.cpp index 712a9d3e..07a2efd2 100644 --- a/launcher/ui/themes/DarkTheme.cpp +++ b/launcher/ui/themes/DarkTheme.cpp @@ -1,5 +1,7 @@ #include "DarkTheme.h" +#include <QObject> + QString DarkTheme::id() { return "dark"; @@ -18,18 +20,18 @@ bool DarkTheme::hasColorScheme() QPalette DarkTheme::colorScheme() { QPalette darkPalette; - darkPalette.setColor(QPalette::Window, QColor(49,54,59)); + darkPalette.setColor(QPalette::Window, QColor(49,49,49)); darkPalette.setColor(QPalette::WindowText, Qt::white); - darkPalette.setColor(QPalette::Base, QColor(35,38,41)); - darkPalette.setColor(QPalette::AlternateBase, QColor(49,54,59)); + darkPalette.setColor(QPalette::Base, QColor(34,34,34)); + darkPalette.setColor(QPalette::AlternateBase, QColor(42,42,42)); darkPalette.setColor(QPalette::ToolTipBase, Qt::white); darkPalette.setColor(QPalette::ToolTipText, Qt::white); darkPalette.setColor(QPalette::Text, Qt::white); - darkPalette.setColor(QPalette::Button, QColor(49,54,59)); + darkPalette.setColor(QPalette::Button, QColor(48,48,48)); darkPalette.setColor(QPalette::ButtonText, Qt::white); darkPalette.setColor(QPalette::BrightText, Qt::red); - darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::Link, QColor(47,163,198)); + darkPalette.setColor(QPalette::Highlight, QColor(145,205,92)); darkPalette.setColor(QPalette::HighlightedText, Qt::black); darkPalette.setColor(QPalette::PlaceholderText, Qt::darkGray); return fadeInactive(darkPalette, fadeAmount(), fadeColor()); @@ -42,7 +44,7 @@ double DarkTheme::fadeAmount() QColor DarkTheme::fadeColor() { - return QColor(49,54,59); + return QColor(49,49,49); } bool DarkTheme::hasStyleSheet() diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 9e0553f8..fdc581b4 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -97,18 +97,7 @@ void InfoFrame::updateWithResource(const Resource& resource) setImage(); } -// https://www.sportskeeda.com/minecraft-wiki/color-codes -static const QMap<QChar, QString> s_value_to_color = { - {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"}, - {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"}, - {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"}, - {'f', "#FFFFFF"} -}; - -void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) -{ - setName(resource_pack.name()); - +QString InfoFrame::renderColorCodes(QString input) { // We have to manually set the colors for use. // // A color is set using §x, with x = a hex number from 0 to f. @@ -116,42 +105,73 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) // We traverse the description and, when one of those is found, we create // a span element with that color set. // - // TODO: Make the same logic for font formatting too. // TODO: Wrap links inside <a> tags - auto description = resource_pack.description(); - - QString description_parsed("<html>"); - bool in_div = false; - - auto desc_it = description.constBegin(); - while (desc_it != description.constEnd()) { - if (*desc_it == u'§') { - if (in_div) - description_parsed += "</span>"; - - auto const& num = *(++desc_it); - description_parsed += QString("<span style=\"color: %1;\">").arg(s_value_to_color.constFind(num).value()); - - in_div = true; - - desc_it++; + // https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes + const QMap<QChar, QString> color_codes_map = { + {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"}, + {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"}, + {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"}, + {'f', "#FFFFFF"} + }; + // https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes + const QMap<QChar, QString> formatting_codes_map = { + {'l', "b"}, {'m', "s"}, {'n', "u"}, {'o', "i"} + }; + + QString html("<html>"); + QList<QString> tags{}; + + auto it = input.constBegin(); + while (it != input.constEnd()) { + // is current char § and is there a following char + if (*it == u'§' && (it + 1) != input.constEnd()) { + auto const& code = *(++it); // incrementing here! + + auto const color_entry = color_codes_map.constFind(code); + auto const tag_entry = formatting_codes_map.constFind(code); + + if (color_entry != color_codes_map.constEnd()) { // color code + html += QString("<span style=\"color: %1;\">").arg(color_entry.value()); + tags << "span"; + } else if (tag_entry != formatting_codes_map.constEnd()) { // formatting code + html += QString("<%1>").arg(tag_entry.value()); + tags << tag_entry.value(); + } else if (code == 'r') { // reset all formatting + while (!tags.isEmpty()) { + html += QString("</%1>").arg(tags.takeLast()); + } + } else { // pass unknown codes through + html += QString("§%1").arg(code); + } + } else { + html += *it; } - - description_parsed += *desc_it; - desc_it++; + it++; } + while (!tags.isEmpty()) { + html += QString("</%1>").arg(tags.takeLast()); + } + html += "</html>"; - if (in_div) - description_parsed += "</span>"; - description_parsed += "</html>"; - - description_parsed.replace("\n", "<br>"); + html.replace("\n", "<br>"); + return html; +} - setDescription(description_parsed); +void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) +{ + setName(renderColorCodes(resource_pack.name())); + setDescription(renderColorCodes(resource_pack.description())); setImage(resource_pack.image({64, 64})); } +void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) +{ + setName(renderColorCodes(texture_pack.name())); + setDescription(renderColorCodes(texture_pack.description())); + setImage(texture_pack.image({64, 64})); +} + void InfoFrame::clear() { setName(); diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 70d15b1e..84523e28 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -19,6 +19,7 @@ #include "minecraft/mod/Mod.h" #include "minecraft/mod/ResourcePack.h" +#include "minecraft/mod/TexturePack.h" namespace Ui { @@ -41,6 +42,9 @@ class InfoFrame : public QFrame { void updateWithMod(Mod const& m); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); + void updateWithTexturePack(TexturePack& tp); + + static QString renderColorCodes(QString input); public slots: void descriptionEllipsisHandler(QString link); diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 56ae35fb..01be88d9 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -14,9 +14,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o QStyleOptionViewItem opt(option); initStyleOption(&opt, index); - auto& rect = opt.rect; - auto icon_width = rect.height(), icon_height = rect.height(); - auto remaining_width = rect.width() - icon_width; + auto rect = opt.rect; if (opt.state & QStyle::State_Selected) { painter->fillRect(rect, opt.palette.highlight()); @@ -25,11 +23,34 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->fillRect(rect, opt.palette.window()); } - { // Icon painting - // Square-sized, occupying the left portion - opt.icon.paint(painter, rect.x(), rect.y(), icon_width, icon_height); + // The default icon size will be a square (and height is usually the lower value). + auto icon_width = rect.height(), icon_height = rect.height(); + int icon_x_margin = (rect.height() - icon_width) / 2; + int icon_y_margin = (rect.height() - icon_height) / 2; + + if (!opt.icon.isNull()) { // Icon painting + { + auto icon_size = opt.decorationSize; + icon_width = icon_size.width(); + icon_height = icon_size.height(); + + icon_x_margin = (rect.height() - icon_width) / 2; + icon_y_margin = (rect.height() - icon_height) / 2; + } + + // Centralize icon with a margin to separate from the other elements + int x = rect.x() + icon_x_margin; + int y = rect.y() + icon_y_margin; + + // Prevent 'scaling null pixmap' warnings + if (icon_width > 0 && icon_height > 0) + opt.icon.paint(painter, x, y, icon_width, icon_height); } + // Change the rect so that funther painting is easier + auto remaining_width = rect.width() - icon_width - 2 * icon_x_margin; + rect.setRect(rect.x() + icon_width + 2 * icon_x_margin, rect.y(), remaining_width, rect.height()); + { // Title painting auto title = index.data(UserDataTypes::TITLE).toString(); @@ -46,7 +67,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->setFont(font); // On the top, aligned to the left after the icon - painter->drawText(rect.x() + icon_width, rect.y() + QFontMetrics(font).height(), title); + painter->drawText(rect.x(), rect.y() + QFontMetrics(font).height(), title); painter->restore(); } @@ -70,7 +91,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o } // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare) - painter->drawText(rect.x() + icon_width, rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width, + painter->drawText(rect.x(), rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width, 2 * opt.fontMetrics.height(), Qt::TextWordWrap, description); } |