diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml new file mode 100644 index 00000000..097524cf --- /dev/null +++ b/.github/workflows/dispatch.yml @@ -0,0 +1,20 @@ +name: Dispatcher +on: + push: + branches: ['6'] +jobs: + dispatch: + name: Dispatch + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Extract branch name + shell: bash + run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >>$GITHUB_OUTPUT + id: extract_branch + - name: Dispatch to workflows + run: | + curl -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token ${{ secrets.DISPATCH_TOKEN }}" \ + --request POST \ + --data '{"event_type": "push_to_main_repo", "client_payload": { "branch": "${{ steps.extract_branch.outputs.branch }}" }}' https://api.github.com/repos/MultiMC/Build/dispatches diff --git a/CMakeLists.txt b/CMakeLists.txt index d005081c..fc1e1eb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,7 +195,7 @@ if(Launcher_LAYOUT_REAL STREQUAL "mac-bundle") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) - set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2021 ${Launcher_Copyright}") + set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2023 ${Launcher_Copyright}") # directories to look for dependencies set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) diff --git a/README.md b/README.md index b158625c..9a609872 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ If you want to contribute, talk to us on [Discord](https://discord.gg/multimc) f While blindly submitting PRs is definitely possible, they're not necessarily going to get accepted. -We aren't looking for flashy features, but expanding upon the existing feature set without distruption or endangering future viability of the project is OK. +We aren't looking for flashy features, but expanding upon the existing feature set without disruption or endangering the future viability of the project is OK. ### Building If you want to build the launcher yourself, check [BUILD.md](BUILD.md) for build instructions. @@ -40,9 +40,9 @@ Unless required by applicable law or agreed to in writing, software distributed ## Forking/Redistributing/Custom builds policy We keep Launcher open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license. -The license gives you access to the source MultiMC is build from, but: -- Not the name, logo and other branding. -- Not the API tokens required to talk to services the launcher depends on. +The license gives you access to the source MultiMC is built from, but not: +- The name, logo and other branding. +- The API tokens required to talk to services that the launcher depends on. Because of the nature of the agreements required to interact with the Microsoft identity platform, it's impossible for us to continue allowing everyone to build the code as 'MultiMC'. The source code has been debranded and now builds as `DevLauncher` by default. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 9e449aec..8c04dd4d 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -57,7 +57,7 @@ QString Config::printableVersionString() const QString vstr = QString("%1.%2.%3").arg(QString::number(VERSION_MAJOR), QString::number(VERSION_MINOR), QString::number(VERSION_HOTFIX)); // If the build is not a main release, append the channel - if(VERSION_CHANNEL != "stable") + if(VERSION_CHANNEL != "develop") { vstr += "-" + VERSION_CHANNEL; } diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 903827ea..7ab5b100 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -100,6 +100,12 @@ public: QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; + /** + * The build that is reported to the Technic API. + */ + QString TECHNIC_API_BUILD = "multimc"; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). diff --git a/launcher/Application.cpp b/launcher/Application.cpp index dc3b9a75..6dd01519 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -575,7 +575,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) FS::updateTimestamp(m_rootPath); #endif - qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; + qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2023 " << BuildConfig.LAUNCHER_COPYRIGHT; qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; @@ -627,7 +627,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this)); // Updates - m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL); m_settings->registerSetting("AutoUpdate", true); // Theming @@ -718,6 +717,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("RecordGameTime", true); + m_settings->registerSetting("ShowGameTimeHours", false); // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); @@ -811,7 +811,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM); auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json"; qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl; - m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); + m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_BUILD)); qDebug() << "<> Updater started."; } diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 374d4a29..928a70a5 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +56,14 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); + + // Managed Packs + m_settings->registerSetting("ManagedPack", false); + m_settings->registerSetting("ManagedPackType", ""); + m_settings->registerSetting("ManagedPackID", ""); + m_settings->registerSetting("ManagedPackName", ""); + m_settings->registerSetting("ManagedPackVersionID", ""); + m_settings->registerSetting("ManagedPackVersionName", ""); } QString BaseInstance::getPreLaunchCommand() @@ -72,6 +81,46 @@ QString BaseInstance::getPostExitCommand() return settings()->get("PostExitCommand").toString(); } +bool BaseInstance::isManagedPack() +{ + return settings()->get("ManagedPack").toBool(); +} + +QString BaseInstance::getManagedPackType() +{ + return settings()->get("ManagedPackType").toString(); +} + +QString BaseInstance::getManagedPackID() +{ + return settings()->get("ManagedPackID").toString(); +} + +QString BaseInstance::getManagedPackName() +{ + return settings()->get("ManagedPackName").toString(); +} + +QString BaseInstance::getManagedPackVersionID() +{ + return settings()->get("ManagedPackVersionID").toString(); +} + +QString BaseInstance::getManagedPackVersionName() +{ + return 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); +} + int BaseInstance::getConsoleMaxLines() const { auto lineSetting = settings()->getSetting("ConsoleMaxLines"); @@ -264,6 +313,11 @@ QString BaseInstance::windowTitle() const return BuildConfig.LAUNCHER_NAME + ": " + name().replace(QRegExp("[ \n\r\t]+"), " "); } +QString BaseInstance::instanceTitle() const +{ + return name().replace(QRegExp("[ \n\r\t]+"), " "); +} + // FIXME: why is this here? move it to MinecraftInstance!!! QStringList BaseInstance::extraArguments() const { diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 488f2781..c148f37e 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,6 +110,8 @@ public: /// Value used for instance window titles QString windowTitle() const; + QString instanceTitle() const; + QString iconKey() const; void setIconKey(QString val); @@ -119,6 +122,14 @@ public: QString getPostExitCommand(); QString getWrapperCommand(); + bool isManagedPack(); + QString getManagedPackType(); + QString getManagedPackID(); + QString getManagedPackName(); + QString getManagedPackVersionID(); + QString getManagedPackVersionName(); + void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); + /// 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 25c28063..95db0882 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -509,6 +509,8 @@ set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.cpp modplatform/technic/SolderPackInstallTask.h modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/SolderPackManifest.h + modplatform/technic/SolderPackManifest.cpp modplatform/technic/TechnicPackProcessor.h modplatform/technic/TechnicPackProcessor.cpp ) @@ -525,6 +527,10 @@ set(ATLAUNCHER_SOURCES set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackManifest.cpp modplatform/modrinth/ModrinthPackManifest.h + modplatform/modrinth/ModrinthInstanceExportTask.h + modplatform/modrinth/ModrinthInstanceExportTask.cpp + modplatform/modrinth/ModrinthHashLookupRequest.h + modplatform/modrinth/ModrinthHashLookupRequest.cpp ) add_unit_test(Index @@ -782,6 +788,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/CreateShortcutDialog.cpp ui/dialogs/CreateShortcutDialog.h + ui/dialogs/ModrinthExportDialog.cpp + ui/dialogs/ModrinthExportDialog.h # GUI - widgets ui/widgets/Common.cpp @@ -880,6 +888,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/CreateShortcutDialog.ui + ui/dialogs/ModrinthExportDialog.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1cdcb4a6..a90166fa 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -311,7 +311,14 @@ void InstanceImportTask::processModrinth() { auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); for(auto & obj: jsonFiles) { Modrinth::File file; - file.path = Json::requireString(obj, "path"); + auto dirtyPath = Json::requireString(obj, "path"); + dirtyPath.replace('\\', '/'); + auto simplifiedPath = QDir::cleanPath(dirtyPath); + QFileInfo fileInfo (simplifiedPath); + if(simplifiedPath.startsWith("../") || simplifiedPath.contains("/../") || fileInfo.isAbsolute()) { + throw JSONValidationError("Invalid path found in modpack files:\n\n" + simplifiedPath); + } + file.path = simplifiedPath; // env doesn't have to be present, in that case mod is required auto env = Json::ensureObject(obj, "env"); diff --git a/launcher/Json.cpp b/launcher/Json.cpp index 37ada1aa..8f6908d3 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -78,6 +78,14 @@ QJsonObject requireObject(const QJsonDocument &doc, const QString &what) } return doc.object(); } +QJsonObject requireObject(const QJsonValueRef &node, const QString &what) +{ + if (!node.isObject()) + { + throw JsonException(what + " is not an object"); + } + return node.toObject(); +} QJsonArray requireArray(const QJsonDocument &doc, const QString &what) { if (!doc.isArray()) diff --git a/launcher/Json.h b/launcher/Json.h index dd70bf56..359f4757 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -41,6 +41,8 @@ QJsonDocument requireDocument(const QString &filename, const QString &what = "Do /// @throw JsonException QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document"); /// @throw JsonException +QJsonObject requireObject(const QJsonValueRef &node, const QString &what = "Node"); +/// @throw JsonException QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document"); /////////////////// WRITING //////////////////// diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 17f6400b..32c5f49b 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -9,6 +9,8 @@ #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" +#include "ui/dialogs/LoginDialog.h" +#include "ui/dialogs/MSALoginDialog.h" #include #include @@ -223,16 +225,60 @@ void LaunchController::login() { } */ case AccountState::Expired: { - auto errorString = tr("The account has expired and needs to be logged into manually again."); - QMessageBox::warning( + auto errorString = tr("The account has expired and needs to be logged into manually. Press OK to log in again."); + auto button = QMessageBox::warning( m_parentWidget, tr("Account refresh failed"), errorString, - QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, QMessageBox::StandardButton::Ok ); - emitFailed(errorString); - return; + if (button == QMessageBox::StandardButton::Ok) { + auto accounts = APPLICATION->accounts(); + bool isDefault = accounts->defaultAccount() == m_accountToUse; + bool msa = m_accountToUse->isMSA(); + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(m_accountToUse->profileId()))); + MinecraftAccountPtr newAccount = nullptr; + if (msa) { + if(BuildConfig.BUILD_PLATFORM == "osx64") { + CustomMessageBox::selectable( + m_parentWidget, + tr("Microsoft Accounts not available"), + tr( + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" + "Please update both your operating system and MultiMC." + ), + QMessageBox::Warning + )->exec(); + emitFailed(tr("Attempted to re-login to a Microsoft account on an unsupported platform")); + return; + } + newAccount = MSALoginDialog::newAccount( + m_parentWidget, + tr("Please enter your Mojang account email and password to add your account.") + ); + } else { + newAccount = LoginDialog::newAccount( + m_parentWidget, + tr("Please enter your Mojang account email and password to add your account.") + ); + } + if (newAccount) { + accounts->addAccount(newAccount); + if (isDefault) { + accounts->setDefaultAccount(newAccount); + } + m_accountToUse = nullptr; + decideAccount(); + continue; + } else { + emitFailed(tr("Account expired and re-login attempt failed")); + return; + } + } else { + emitFailed(errorString); + return; + } } case AccountState::Gone: { auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to."); diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 4d7f424d..387ecf6d 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -36,3 +36,7 @@ QString Time::prettifyDuration(int64_t duration) { } return QObject::tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes); } + +QString Time::prettifyDurationHours(int64_t duration) { + return QString("%1").arg(duration / 3600.0, 0, 'f', 0); +} diff --git a/launcher/MMCTime.h b/launcher/MMCTime.h index 10ff2ffe..ae1fa9a8 100644 --- a/launcher/MMCTime.h +++ b/launcher/MMCTime.h @@ -21,5 +21,6 @@ namespace Time { QString prettifyDuration(int64_t duration); +QString prettifyDurationHours(int64_t duration); } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index ae0fbd31..19256e30 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -48,6 +48,7 @@ #include "MinecraftLoadAndCheck.h" #include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" +#include "minecraft/VersionFilterData.h" #define IBUS "@im=ibus" @@ -425,10 +426,17 @@ QStringList MinecraftInstance::processMinecraftArgs( if (serverToJoin && !serverToJoin->address.isEmpty()) { - args_pattern += " --server " + serverToJoin->address; - args_pattern += " --port " + QString::number(serverToJoin->port); + if (m_components->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + { + args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ":" + QString::number(serverToJoin->port); + } + else + { + args_pattern += " --server " + serverToJoin->address; + args_pattern += " --port " + QString::number(serverToJoin->port); + } } - + QMap token_mapping; // yggdrasil! if(session) { @@ -489,6 +497,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS if (serverToJoin && !serverToJoin->address.isEmpty()) { + launchScript += "useQuickPlay " + QString::number(m_components->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + "\n"; launchScript += "serverAddress " + serverToJoin->address + "\n"; launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n"; } @@ -513,6 +522,8 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS .arg(settings()->get("MinecraftWinHeight").toInt()); launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; + launchScript += "instanceTitle " + instanceTitle() + "\n"; + launchScript += "instanceIconId " + iconKey() + "\n"; } // legacy auth @@ -785,11 +796,19 @@ QString MinecraftInstance::getStatusbarDescription() if(m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0) { - description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed()))); + if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) { + description.append(tr(", last played for %1 hours").arg(Time::prettifyDurationHours(lastTimePlayed()))); + } else { + description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed()))); + } } if (totalTimePlayed() > 0) { - description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed()))); + if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) { + description.append(tr(", total played for %1 hours").arg(Time::prettifyDurationHours(totalTimePlayed()))); + } else { + description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed()))); + } } } if(hasCrashed()) diff --git a/launcher/minecraft/VersionFilterData.cpp b/launcher/minecraft/VersionFilterData.cpp index c286d266..64ae488c 100644 --- a/launcher/minecraft/VersionFilterData.cpp +++ b/launcher/minecraft/VersionFilterData.cpp @@ -66,7 +66,8 @@ VersionFilterData::VersionFilterData() "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"}; - java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); - java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); - java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); + java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); + quickPlayBeginsDate = timeFromS3Time("2023-04-05T12:05:17+00:00"); } diff --git a/launcher/minecraft/VersionFilterData.h b/launcher/minecraft/VersionFilterData.h index 13445a51..8ff0a00c 100644 --- a/launcher/minecraft/VersionFilterData.h +++ b/launcher/minecraft/VersionFilterData.h @@ -27,5 +27,7 @@ struct VersionFilterData QDateTime java16BeginsDate; // release data of first version to require Java 17 (1.18 Pre Release 2) QDateTime java17BeginsDate; + // release date of first version to use --quickPlayMultiplayer instead of --server/--port for directly joining servers + QDateTime quickPlayBeginsDate; }; extern VersionFilterData g_VersionFilterData; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index c4e197b4..79fc5d86 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -39,10 +39,11 @@ namespace ATLauncher { -PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version) { m_support = support; - m_pack = pack; + m_pack_name = packName; + m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); m_version_name = version; } @@ -60,7 +61,7 @@ void PackInstallTask::executeTask() qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network()); auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") - .arg(m_pack).arg(m_version_name); + .arg(m_pack_safe_name).arg(m_version_name); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -96,6 +97,10 @@ void PackInstallTask::onDownloadSucceeded() } m_version = version; + // Display install message if one exists + if (!m_version.messages.install.isEmpty()) + m_support->displayMessage(m_version.messages.install); + auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); if(!vlist) { @@ -303,7 +308,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto f = std::make_shared(); - f->name = m_pack + " " + m_version_name + " (libraries)"; + f->name = m_pack_name + " " + m_version_name + " (libraries)"; for(const auto & lib : m_version.libraries) { auto libName = detectLibrary(lib); @@ -408,7 +413,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< } auto f = std::make_shared(); - f->name = m_pack + " " + m_version_name; + f->name = m_pack_name + " " + m_version_name; if(!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { f->mainClass = mainClass; } @@ -450,9 +455,9 @@ void PackInstallTask::installConfigs() setStatus(tr("Downloading configs...")); jobPtr = new NetJob(tr("Config download"), APPLICATION->network()); - auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); + auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") - .arg(m_pack).arg(m_version_name); + .arg(m_pack_safe_name).arg(m_version_name); auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); @@ -526,7 +531,7 @@ void PackInstallTask::downloadMods() QVector selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); - selectedMods = m_support->chooseOptionalMods(optionalMods); + selectedMods = m_support->chooseOptionalMods(m_version, optionalMods); } setStatus(tr("Downloading mods...")); @@ -810,6 +815,7 @@ void PackInstallTask::install() instance.setName(m_instName); instance.setIconKey(m_instIcon); + instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); instanceSettings->resumeSave(); jarmods.clear(); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 783ec19b..44bf01e7 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020-2022 Jamie Mansfield * Copyright 2021 Petr Mrazek * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,7 @@ public: /** * Requests a user interaction to select which optional mods should be installed. */ - virtual QVector chooseOptionalMods(QVector mods) = 0; + virtual QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) = 0; /** * Requests a user interaction to select a component version from a given version list @@ -45,6 +45,11 @@ public: */ virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + /** + * Requests a user interaction to display a message. + */ + virtual void displayMessage(QString message) = 0; + }; class PackInstallTask : public InstanceTask @@ -52,7 +57,7 @@ class PackInstallTask : public InstanceTask Q_OBJECT public: - explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version); + explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version); virtual ~PackInstallTask(){} bool canAbort() const override { return true; } @@ -94,7 +99,8 @@ private: NetJob::Ptr jobPtr; QByteArray response; - QString m_pack; + QString m_pack_name; + QString m_pack_safe_name; QString m_version_name; PackVersion m_version; diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index b214f738..f8f7c2f4 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -178,6 +178,8 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.depends.append(Json::requireValueString(depends)); } } + p.colour = Json::ensureString(obj, QString("colour"), ""); + p.warning = Json::ensureString(obj, QString("warning"), ""); p.client = Json::ensureBoolean(obj, QString("client"), false); @@ -197,6 +199,12 @@ static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments & a, a.depends = Json::ensureString(obj, "depends", ""); } +static void loadVersionMessages(ATLauncher::VersionMessages & m, QJsonObject & obj) +{ + m.install = Json::ensureString(obj, "install", ""); + m.update = Json::ensureString(obj, "update", ""); +} + void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) { v.version = Json::requireString(obj, "version"); @@ -244,4 +252,25 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) auto configsObj = Json::requireObject(obj, "configs"); loadVersionConfigs(v.configs, configsObj); } + + if(obj.contains("colours")) { + auto colourObj = Json::requireObject(obj, "colours"); + + for (const auto &key : colourObj.keys()) { + v.colours[key] = Json::requireValueString(colourObj.value(key), "colour"); + } + } + + if(obj.contains("warnings")) { + auto warningsObj = Json::requireObject(obj, "warnings"); + + for (const auto &key : warningsObj.keys()) { + v.warnings[key] = Json::requireValueString(warningsObj.value(key), "warning"); + } + } + + if(obj.contains("messages")) { + auto messages = Json::requireObject(obj, "messages"); + loadVersionMessages(v.messages, messages); + } } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 6be82da4..c7ad1c67 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -16,8 +16,10 @@ #pragma once +#include #include #include +#include #include namespace ATLauncher @@ -109,6 +111,8 @@ struct VersionMod bool library; QString group; QVector depends; + QString colour; + QString warning; bool client; @@ -134,6 +138,12 @@ struct PackVersionExtraArguments QString depends; }; +struct VersionMessages +{ + QString install; + QString update; +}; + struct PackVersion { QString version; @@ -146,6 +156,10 @@ struct PackVersion QVector libraries; QVector mods; VersionConfigs configs; + + QMap colours; + QMap warnings; + VersionMessages messages; }; void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/launcher/modplatform/modrinth/ModrinthHashLookupRequest.cpp b/launcher/modplatform/modrinth/ModrinthHashLookupRequest.cpp new file mode 100644 index 00000000..4edb9128 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthHashLookupRequest.cpp @@ -0,0 +1,124 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include "ModrinthHashLookupRequest.h" +#include "BuildConfig.h" +#include "Json.h" + +namespace Modrinth +{ + +HashLookupRequest::HashLookupRequest(QList hashes, QList *output) : NetAction(), m_hashes(hashes), m_output(output) +{ + m_url = "https://api.modrinth.com/v2/version_files"; + m_status = Job_NotStarted; +} + +void HashLookupRequest::startImpl() +{ + finished = false; + m_status = Job_InProgress; + + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QJsonObject requestObject; + QJsonArray hashes; + + for (const auto &data : m_hashes) { + hashes.append(data.hash); + } + + requestObject.insert("hashes", hashes); + requestObject.insert("algorithm", QJsonValue("sha512")); + + QNetworkReply *rep = m_network->post(request, QJsonDocument(requestObject).toJson()); + m_reply.reset(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &HashLookupRequest::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &HashLookupRequest::downloadFinished); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +} + +void HashLookupRequest::downloadError(QNetworkReply::NetworkError error) +{ + qCritical() << "Modrinth hash lookup request failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll(); + if (finished) { + qCritical() << "Double finished ModrinthHashLookupRequest!"; + return; + } + m_status = Job_Failed; + finished = true; + m_reply.reset(); + emit failed(m_index_within_job); +} + +void HashLookupRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} + +void HashLookupRequest::downloadFinished() +{ + if (finished) { + qCritical() << "Double finished ModrinthHashLookupRequest!"; + return; + } + + QByteArray data = m_reply->readAll(); + m_reply.reset(); + + try { + auto document = Json::requireDocument(data); + auto rootObject = Json::requireObject(document); + + for (const auto &hashData : m_hashes) { + if (rootObject.contains(hashData.hash)) { + auto versionObject = Json::requireObject(rootObject, hashData.hash); + + auto files = Json::requireIsArrayOf(versionObject, "files"); + + QJsonObject file; + + for (const auto &fileJson : files) { + auto hashes = Json::requireObject(fileJson, "hashes"); + QString sha512 = Json::requireString(hashes, "sha512"); + + if (sha512 == hashData.hash) { + file = fileJson; + } + } + + m_output->append(HashLookupResponseData { + hashData.fileInfo, + true, + file + }); + } else { + m_output->append(HashLookupResponseData { + hashData.fileInfo, + false, + QJsonObject() + }); + } + } + + m_status = Job_Finished; + finished = true; + emit succeeded(m_index_within_job); + } catch (const Json::JsonException &e) { + qCritical() << "Failed to parse Modrinth hash lookup response: " << e.cause(); + m_status = Job_Failed; + finished = true; + emit failed(m_index_within_job); + } +} +} \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthHashLookupRequest.h b/launcher/modplatform/modrinth/ModrinthHashLookupRequest.h new file mode 100644 index 00000000..a3a803f1 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthHashLookupRequest.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include +#include "net/NetAction.h" + +namespace Modrinth +{ + +struct HashLookupData +{ + QFileInfo fileInfo; + QString hash; +}; + +struct HashLookupResponseData +{ + QFileInfo fileInfo; + bool found; + QJsonObject fileJson; +}; + +class HashLookupRequest : public NetAction +{ +public: + using Ptr = shared_qobject_ptr; + + explicit HashLookupRequest(QList hashes, QList *output); + static Ptr make(QList hashes, QList *output) { + return Ptr(new HashLookupRequest(hashes, output)); + } + +protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void downloadFinished() override; + void downloadReadyRead() override {} + +public slots: + void startImpl() override; + +private: + QList m_hashes; + std::shared_ptr> m_output; + bool finished = true; +}; + +} \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthInstanceExportTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceExportTask.cpp new file mode 100644 index 00000000..7f33c4de --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthInstanceExportTask.cpp @@ -0,0 +1,252 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include +#include "Json.h" +#include "ModrinthInstanceExportTask.h" +#include "net/NetJob.h" +#include "Application.h" +#include "ui/dialogs/ModrinthExportDialog.h" +#include "JlCompress.h" +#include "FileSystem.h" +#include "ModrinthHashLookupRequest.h" + +namespace Modrinth +{ + +InstanceExportTask::InstanceExportTask(InstancePtr instance, ExportSettings settings) : m_instance(instance), m_settings(settings) {} + +void InstanceExportTask::executeTask() +{ + setStatus(tr("Finding files to look up on Modrinth...")); + + QDir modsDir(m_instance->gameRoot() + "/mods"); + modsDir.setFilter(QDir::Files); + modsDir.setNameFilters(QStringList() << "*.jar"); + + QDir resourcePacksDir(m_instance->gameRoot() + "/resourcepacks"); + resourcePacksDir.setFilter(QDir::Files); + resourcePacksDir.setNameFilters(QStringList() << "*.zip"); + + QDir shaderPacksDir(m_instance->gameRoot() + "/shaderpacks"); + shaderPacksDir.setFilter(QDir::Files); + shaderPacksDir.setNameFilters(QStringList() << "*.zip"); + + QStringList filesToResolve; + + if (modsDir.exists()) { + QDirIterator modsIterator(modsDir); + while (modsIterator.hasNext()) { + filesToResolve << modsIterator.next(); + } + } + + if (m_settings.includeResourcePacks && resourcePacksDir.exists()) { + QDirIterator resourcePacksIterator(resourcePacksDir); + while (resourcePacksIterator.hasNext()) { + filesToResolve << resourcePacksIterator.next(); + } + } + + if (m_settings.includeShaderPacks && shaderPacksDir.exists()) { + QDirIterator shaderPacksIterator(shaderPacksDir); + while (shaderPacksIterator.hasNext()) { + filesToResolve << shaderPacksIterator.next(); + } + } + + if (!m_settings.datapacksPath.isEmpty()) { + QDir datapacksDir(m_instance->gameRoot() + "/" + m_settings.datapacksPath); + datapacksDir.setFilter(QDir::Files); + datapacksDir.setNameFilters(QStringList() << "*.zip"); + + if (datapacksDir.exists()) { + QDirIterator datapacksIterator(datapacksDir); + while (datapacksIterator.hasNext()) { + filesToResolve << datapacksIterator.next(); + } + } + } + + m_netJob = new NetJob(tr("Modrinth pack export"), APPLICATION->network()); + + QList hashes; + + qint64 progress = 0; + setProgress(progress, filesToResolve.length()); + for (const QString &filePath: filesToResolve) { + qDebug() << "Attempting to resolve file hash from Modrinth API: " << filePath; + QFile file(filePath); + + if (file.open(QFile::ReadOnly)) { + QByteArray contents = file.readAll(); + QCryptographicHash hasher(QCryptographicHash::Sha512); + hasher.addData(contents); + QString hash = hasher.result().toHex(); + + hashes.append(HashLookupData { + QFileInfo(file), + hash + }); + + progress++; + setProgress(progress, filesToResolve.length()); + } + } + + m_response.reset(new QList); + + m_netJob->addNetAction(HashLookupRequest::make(hashes, m_response.get())); + + connect(m_netJob.get(), &NetJob::succeeded, this, &InstanceExportTask::lookupSucceeded); + connect(m_netJob.get(), &NetJob::failed, this, &InstanceExportTask::lookupFailed); + connect(m_netJob.get(), &NetJob::progress, this, &InstanceExportTask::lookupProgress); + + m_netJob->start(); + setStatus(tr("Looking up files on Modrinth...")); +} + +void InstanceExportTask::lookupSucceeded() +{ + setStatus(tr("Creating modpack metadata...")); + QList resolvedFiles; + QFileInfoList failedFiles; + + for (const auto &file : *m_response) { + if (file.found) { + try { + auto url = Json::requireString(file.fileJson, "url"); + auto hashes = Json::requireObject(file.fileJson, "hashes"); + + QString sha512Hash = Json::requireString(hashes, "sha512"); + QString sha1Hash = Json::requireString(hashes, "sha1"); + + ExportFile fileData; + + QDir gameDir(m_instance->gameRoot()); + + fileData.path = gameDir.relativeFilePath(file.fileInfo.absoluteFilePath()); + fileData.download = url; + fileData.sha512 = sha512Hash; + fileData.sha1 = sha1Hash; + fileData.fileSize = file.fileInfo.size(); + + resolvedFiles << fileData; + } catch (const Json::JsonException &e) { + qDebug() << "File " << file.fileInfo.absoluteFilePath() << " failed to process for reason " << e.cause() << ", adding to overrides"; + failedFiles << file.fileInfo; + } + } else { + failedFiles << file.fileInfo; + } + } + + QJsonObject indexJson; + indexJson.insert("formatVersion", QJsonValue(1)); + indexJson.insert("game", QJsonValue("minecraft")); + indexJson.insert("versionId", QJsonValue(m_settings.version)); + indexJson.insert("name", QJsonValue(m_settings.name)); + + if (!m_settings.description.isEmpty()) { + indexJson.insert("summary", QJsonValue(m_settings.description)); + } + + QJsonArray files; + + for (const auto &file : resolvedFiles) { + QJsonObject fileObj; + fileObj.insert("path", file.path); + + QJsonObject hashes; + hashes.insert("sha512", file.sha512); + hashes.insert("sha1", file.sha1); + fileObj.insert("hashes", hashes); + + QJsonArray downloads; + downloads.append(file.download); + fileObj.insert("downloads", downloads); + + fileObj.insert("fileSize", QJsonValue(file.fileSize)); + + files.append(fileObj); + } + + indexJson.insert("files", files); + + QJsonObject dependencies; + dependencies.insert("minecraft", m_settings.gameVersion); + if (!m_settings.forgeVersion.isEmpty()) { + dependencies.insert("forge", m_settings.forgeVersion); + } + if (!m_settings.fabricVersion.isEmpty()) { + dependencies.insert("fabric-loader", m_settings.fabricVersion); + } + if (!m_settings.quiltVersion.isEmpty()) { + dependencies.insert("quilt-loader", m_settings.quiltVersion); + } + + indexJson.insert("dependencies", dependencies); + + setStatus(tr("Copying files to modpack...")); + + QTemporaryDir tmp; + if (tmp.isValid()) { + Json::write(indexJson, tmp.path() + "/modrinth.index.json"); + + if (!failedFiles.isEmpty()) { + QDir tmpDir(tmp.path()); + QDir gameDir(m_instance->gameRoot()); + for (const auto &file : failedFiles) { + QString src = file.absoluteFilePath(); + tmpDir.mkpath("overrides/" + gameDir.relativeFilePath(file.absolutePath())); + QString dest = tmpDir.path() + "/overrides/" + gameDir.relativeFilePath(src); + if (!QFile::copy(file.absoluteFilePath(), dest)) { + emitFailed(tr("Failed to copy file %1 to overrides").arg(src)); + return; + } + } + + if (m_settings.includeGameConfig) { + tmpDir.mkdir("overrides"); + QFile::copy(gameDir.absoluteFilePath("options.txt"), tmpDir.absoluteFilePath("overrides/options.txt")); + } + + if (m_settings.includeModConfigs) { + tmpDir.mkdir("overrides"); + FS::copy copy(m_instance->gameRoot() + "/config", tmpDir.absoluteFilePath("overrides/config")); + copy(); + } + } + + setStatus(tr("Zipping modpack...")); + if (!JlCompress::compressDir(m_settings.exportPath, tmp.path())) { + emitFailed(tr("Failed to create zip file")); + return; + } + } else { + emitFailed(tr("Failed to create temporary directory")); + return; + } + + qDebug() << "Successfully exported Modrinth pack to " << m_settings.exportPath; + emitSucceeded(); +} + +void InstanceExportTask::lookupFailed(const QString &reason) +{ + emitFailed(reason); +} + +void InstanceExportTask::lookupProgress(qint64 current, qint64 total) +{ + setProgress(current, total); +} + +} \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthInstanceExportTask.h b/launcher/modplatform/modrinth/ModrinthInstanceExportTask.h new file mode 100644 index 00000000..4a6010c8 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthInstanceExportTask.h @@ -0,0 +1,72 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "net/NetJob.h" +#include "ui/dialogs/ModrinthExportDialog.h" +#include "ModrinthHashLookupRequest.h" + +namespace Modrinth +{ + +struct ExportSettings +{ + QString version; + QString name; + QString description; + + bool includeGameConfig; + bool includeModConfigs; + bool includeResourcePacks; + bool includeShaderPacks; + QString datapacksPath; + + QString gameVersion; + QString forgeVersion; + QString fabricVersion; + QString quiltVersion; + + QString exportPath; +}; + +// Using the existing Modrinth::File struct from the importer doesn't actually make much sense here (doesn't support multiple hashes, hash is a byte array rather than a string, no file size, etc) +struct ExportFile +{ + QString path; + QString sha512; + QString sha1; + QString download; + qint64 fileSize; +}; + +class InstanceExportTask : public Task +{ +Q_OBJECT + +public: + explicit InstanceExportTask(InstancePtr instance, ExportSettings settings); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private slots: + void lookupSucceeded(); + void lookupFailed(const QString &reason); + void lookupProgress(qint64 current, qint64 total); + +private: + InstancePtr m_instance; + ExportSettings m_settings; + std::shared_ptr> m_response; + NetJob::Ptr m_netJob; +}; + +} \ No newline at end of file diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 4e9c7ad3..ccd0b921 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +21,18 @@ #include #include #include "TechnicPackProcessor.h" +#include "SolderPackManifest.h" +#include "net/ChecksumValidator.h" Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, const QUrl &sourceUrl, + const QString &version, const QString &minecraftVersion ) { m_sourceUrl = sourceUrl; m_minecraftVersion = minecraftVersion; + m_version = version; m_network = network; } @@ -41,34 +46,12 @@ bool Technic::SolderPackInstallTask::abort() { void Technic::SolderPackInstallTask::executeTask() { - setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); - m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); - auto job = m_filesNetJob.get(); - connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded); - connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); - m_filesNetJob->start(); -} + setStatus(tr("Resolving modpack files")); -void Technic::SolderPackInstallTask::versionSucceeded() -{ - try - { - QJsonDocument doc = Json::requireDocument(m_response); - QJsonObject obj = Json::requireObject(doc); - QString version = Json::requireString(obj, "recommended", "__placeholder__"); - m_sourceUrl = m_sourceUrl.toString() + '/' + version; - } - catch (const JSONValidationError &e) - { - emitFailed(e.cause()); - m_filesNetJob.reset(); - return; - } - - setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString())); m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto sourceUrl = QString("%1/%2").arg(m_sourceUrl.toString(), m_version); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); @@ -77,38 +60,47 @@ void Technic::SolderPackInstallTask::versionSucceeded() void Technic::SolderPackInstallTask::fileListSucceeded() { - setStatus(tr("Downloading modpack:")); - QStringList modUrls; - try - { - QJsonDocument doc = Json::requireDocument(m_response); - QJsonObject obj = Json::requireObject(doc); - QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); - if (!minecraftVersion.isEmpty()) - m_minecraftVersion = minecraftVersion; - QJsonArray mods = Json::requireArray(obj, "mods", "'mods'"); - for (auto mod: mods) - { - QJsonObject modObject = Json::requireValueObject(mod); - modUrls.append(Json::requireString(modObject, "url", "'url'")); - } + setStatus(tr("Downloading modpack")); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; } - catch (const JSONValidationError &e) - { - emitFailed(e.cause()); + auto obj = doc.object(); + + TechnicSolder::PackBuild build; + try { + TechnicSolder::loadPackBuild(build, obj); + } + catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); m_filesNetJob.reset(); return; } + + if (!build.minecraft.isEmpty()) + m_minecraftVersion = build.minecraft; + m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); int i = 0; - for (auto &modUrl: modUrls) + for (const auto &mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path)); + + auto dl = Net::Download::makeFile(mod.url, path); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + m_filesNetJob->addNetAction(dl); + i++; } - m_modCount = modUrls.size(); + m_modCount = build.mods.size(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); @@ -206,6 +198,4 @@ void Technic::SolderPackInstallTask::extractFinished() void Technic::SolderPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); - return; } - diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index 9b2058d8..21596783 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +28,7 @@ namespace Technic { Q_OBJECT public: - explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &sourceUrl, const QString &minecraftVersion); + explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &sourceUrl, const QString& version, const QString &minecraftVersion); bool canAbort() const override { return true; } bool abort() override; @@ -37,7 +38,6 @@ namespace Technic virtual void executeTask() override; private slots: - void versionSucceeded(); void fileListSucceeded(); void downloadSucceeded(); void downloadFailed(QString reason); @@ -52,6 +52,7 @@ namespace Technic NetJob::Ptr m_filesNetJob; QUrl m_sourceUrl; + QString m_version; QString m_minecraftVersion; QByteArray m_response; QTemporaryDir m_outputDir; diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp new file mode 100644 index 00000000..ad195d3a --- /dev/null +++ b/launcher/modplatform/technic/SolderPackManifest.cpp @@ -0,0 +1,56 @@ +/* + * Copyright 2022 Jamie Mansfield + * + * 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 "SolderPackManifest.h" + +#include "Json.h" + +namespace TechnicSolder { + +void loadPack(Pack& v, QJsonObject& obj) +{ + v.recommended = Json::requireString(obj, "recommended"); + v.latest = Json::requireString(obj, "latest"); + + auto builds = Json::requireArray(obj, "builds"); + for (const auto buildRaw : builds) { + auto build = Json::requireValueString(buildRaw); + v.builds.append(build); + } +} + +static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) +{ + b.name = Json::requireString(obj, "name"); + b.version = Json::requireString(obj, "version"); + b.md5 = Json::requireString(obj, "md5"); + b.url = Json::requireString(obj, "url"); +} + +void loadPackBuild(PackBuild& v, QJsonObject& obj) +{ + v.minecraft = Json::requireString(obj, "minecraft"); + + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) { + auto modObj = Json::requireValueObject(modRaw); + PackBuildMod mod; + loadPackBuildMod(mod, modObj); + v.mods.append(mod); + } +} + +} diff --git a/launcher/modplatform/technic/SolderPackManifest.h b/launcher/modplatform/technic/SolderPackManifest.h new file mode 100644 index 00000000..b4d8cf53 --- /dev/null +++ b/launcher/modplatform/technic/SolderPackManifest.h @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Jamie Mansfield + * + * 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 +#include +#include + +namespace TechnicSolder { + +struct Pack { + QString recommended; + QString latest; + QVector builds; +}; + +void loadPack(Pack& v, QJsonObject& obj); + +struct PackBuildMod { + QString name; + QString version; + QString md5; + QString url; +}; + +struct PackBuild { + QString minecraft; + QVector mods; +}; + +void loadPackBuild(PackBuild& v, QJsonObject& obj); + +} diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index b314573f..f92d4de7 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2021 MultiMC Contributors +/* Copyright 2013-2023 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,6 +122,13 @@ void Download::downloadError(QNetworkReply::NetworkError error) qCritical() << "Aborted " << m_url.toString(); m_status = Job_Aborted; } + else if(error == QNetworkReply::ContentNotFoundError && (m_options & Option::AllowNotFound)) + { + // The Modrinth API returns a 404 when a hash was not found when performing reverse hash lookup, we don't want to treat this as a failure + qDebug() << "Received 404 from " << m_url.toString() << ", continuing..."; + m_status = Job_Finished; + return; + } else { if(m_options & Option::AcceptLocalFiles) diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 0f9bfe7f..343ce7cd 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2021 MultiMC Contributors +/* Copyright 2013-2023 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,8 @@ public: /* types */ enum class Option { NoOptions = 0, - AcceptLocalFiles = 1 + AcceptLocalFiles = 1, + AllowNotFound = 2 }; Q_DECLARE_FLAGS(Options, Option) diff --git a/launcher/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp index c08bcdcb..d91465c6 100644 --- a/launcher/notifications/NotificationChecker.cpp +++ b/launcher/notifications/NotificationChecker.cpp @@ -10,7 +10,7 @@ #include "Application.h" NotificationChecker::NotificationChecker(QObject *parent) - : QObject(parent) + : QObject(parent), m_appVersionChannel("develop") { } @@ -19,11 +19,6 @@ void NotificationChecker::setNotificationsUrl(const QUrl ¬ificationsUrl) m_notificationsUrl = notificationsUrl; } -void NotificationChecker::setApplicationChannel(QString channel) -{ - m_appVersionChannel = channel; -} - void NotificationChecker::setApplicationFullVersion(QString version) { m_appFullVersion = version; diff --git a/launcher/notifications/NotificationChecker.h b/launcher/notifications/NotificationChecker.h index 0f305f33..4049e55b 100644 --- a/launcher/notifications/NotificationChecker.h +++ b/launcher/notifications/NotificationChecker.h @@ -14,7 +14,6 @@ public: void setNotificationsUrl(const QUrl ¬ificationsUrl); void setApplicationPlatform(QString platform); - void setApplicationChannel(QString channel); void setApplicationFullVersion(QString version); struct NotificationEntry diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 5064eebd..3ce89391 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -192,7 +192,6 @@ void readIndex(const QString & path, QMap& languages) return; } - int index = 1; try { auto toplevel_doc = Json::requireDocument(data); @@ -225,7 +224,6 @@ void readIndex(const QString & path, QMap& languages) lang.file_size = Json::requireInteger(langObj, "size"); languages.insert(lang.key, lang); - index++; } } catch (Json::JsonException & e) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 139ca780..0a747734 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2021 MultiMC Contributors +/* Copyright 2013-2023 MultiMC Contributors * * Authors: Andrew Okin * Peterix @@ -85,6 +85,7 @@ #include "ui/dialogs/NotificationDialog.h" #include "ui/dialogs/CreateShortcutDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/dialogs/ModrinthExportDialog.h" #include "UpdateController.h" #include "KonamiCode.h" @@ -849,14 +850,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // if automatic update checks are allowed, start one. if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed) { - updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); + updater->checkForUpdate(false); } } { auto checker = new NotificationChecker(); checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL)); - checker->setApplicationChannel(BuildConfig.VERSION_CHANNEL); checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM); checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR); m_notificationChecker.reset(checker); @@ -975,6 +975,33 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) void MainWindow::updateToolsMenu() { + QToolButton *exportButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionExportInstance)); + exportButton->setPopupMode(QToolButton::MenuButtonPopup); + + QMenu *exportMenu = ui->actionExportInstance->menu(); + + if (exportMenu) { + exportMenu->clear(); + } else { + exportMenu = new QMenu(); + } + + exportMenu->addSeparator()->setText(tr("Format")); + + QAction *mmcExport = exportMenu->addAction(BuildConfig.LAUNCHER_NAME); + QAction *modrinthExport = exportMenu->addAction(tr("Modrinth (WIP)")); + + connect(mmcExport, &QAction::triggered, this, &MainWindow::on_actionExportInstance_triggered); + connect(modrinthExport, &QAction::triggered, [this]() + { + if (m_selectedInstance) { + ModrinthExportDialog dlg(m_selectedInstance, this); + dlg.exec(); + } + }); + + ui->actionExportInstance->setMenu(exportMenu); + QToolButton *launchButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); QToolButton *launchOfflineButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline)); @@ -1639,7 +1666,7 @@ void MainWindow::checkForUpdates() if(BuildConfig.UPDATER_ENABLED) { auto updater = APPLICATION->updateChecker(); - updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true); + updater->checkForUpdate(true); } else { @@ -2006,6 +2033,10 @@ void MainWindow::updateStatusCenter() int timePlayed = APPLICATION->instances()->getTotalPlayTime(); if (timePlayed > 0) { - m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); + if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) { + m_statusCenter->setText(tr("Total playtime: %1 hours").arg(Time::prettifyDurationHours(timePlayed))); + } else { + m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); + } } } diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 85397fea..6eb5e78e 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -60,9 +60,12 @@ CreateShortcutDialog::~CreateShortcutDialog() void CreateShortcutDialog::on_shortcutPathBrowse_clicked() { QString linkExtension; -#ifdef Q_OS_UNIX +#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) linkExtension = ui->createScriptCheckBox->isChecked() ? "sh" : "desktop"; #endif +#ifdef Q_OS_MAC + linkExtension = "command"; +#endif #ifdef Q_OS_WIN linkExtension = ui->createScriptCheckBox->isChecked() ? "bat" : "lnk"; #endif @@ -104,20 +107,20 @@ void CreateShortcutDialog::updateDialogState() } } -QString CreateShortcutDialog::getLaunchCommand() +QString CreateShortcutDialog::getLaunchCommand(bool escapeQuotesTwice) { - return "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()) + "\"" - + getLaunchArgs(); + return "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).replace('"', escapeQuotesTwice ? "\\\\\"" : "\\\"") + "\"" + + getLaunchArgs(escapeQuotesTwice); } -QString CreateShortcutDialog::getLaunchArgs() +QString CreateShortcutDialog::getLaunchArgs(bool escapeQuotesTwice) { - return " -d \"" + QDir::toNativeSeparators(QDir::currentPath()) + "\"" - + " -l " + m_instance->id() - + (ui->joinServerCheckBox->isChecked() ? " -s " + ui->joinServer->text() : "") - + (ui->useProfileCheckBox->isChecked() ? " -a " + ui->profileComboBox->currentText() : "") + return " -d \"" + QDir::toNativeSeparators(QDir::currentPath()).replace('"', escapeQuotesTwice ? "\\\\\"" : "\\\"") + "\"" + + " -l \"" + m_instance->id() + "\"" + + (ui->joinServerCheckBox->isChecked() ? " -s \"" + ui->joinServer->text() + "\"" : "") + + (ui->useProfileCheckBox->isChecked() ? " -a \"" + ui->profileComboBox->currentText() + "\"" : "") + (ui->launchOfflineCheckBox->isChecked() ? " -o" : "") - + (ui->offlineUsernameCheckBox->isChecked() ? " -n " + ui->offlineUsername->text() : ""); + + (ui->offlineUsernameCheckBox->isChecked() ? " -n \"" + ui->offlineUsername->text() + "\"" : ""); } void CreateShortcutDialog::createShortcut() @@ -134,7 +137,7 @@ void CreateShortcutDialog::createShortcut() { shortcutText = "#!/bin/sh\n" // FIXME: is there a way to use the launcher script instead of the raw binary here? - "cd \"" + QDir::currentPath() + "\"\n" + "cd \"" + QDir::currentPath().replace('"', "\\\"") + "\"\n" + getLaunchCommand() + " &\n"; } else // freedesktop.org desktop entry @@ -149,7 +152,7 @@ void CreateShortcutDialog::createShortcut() shortcutText = "[Desktop Entry]\n" "Type=Application\n" "Name=" + m_instance->name() + " - " + BuildConfig.LAUNCHER_DISPLAYNAME + "\n" - + "Exec=" + getLaunchCommand() + "\n" + + "Exec=" + getLaunchCommand(true) + "\n" + "Path=" + QDir::currentPath() + "\n" + "Icon=" + QDir::currentPath() + "/icons/shortcut-icon.png\n"; @@ -159,7 +162,7 @@ void CreateShortcutDialog::createShortcut() // Windows batch script implementation shortcutText = "@ECHO OFF\r\n" "CD \"" + QDir::toNativeSeparators(QDir::currentPath()) + "\"\r\n" - "START /B " + getLaunchCommand() + "\r\n"; + "START /B \"\" " + getLaunchCommand() + "\r\n"; #endif QFile shortcutFile(ui->shortcutPath->text()); if (shortcutFile.open(QIODevice::WriteOnly)) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index a2497dd6..4714253b 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -39,8 +39,8 @@ private: Ui::CreateShortcutDialog *ui; InstancePtr m_instance; - QString getLaunchCommand(); - QString getLaunchArgs(); + QString getLaunchCommand(bool escapeQuotesTwice = false); + QString getLaunchArgs(bool escapeQuotesTwice = false); void createShortcut(); diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index f46aa3b9..41aac720 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2021 MultiMC Contributors +/* Copyright 2013-2022 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ #include #include +#include MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { @@ -34,6 +35,7 @@ MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MS int MSALoginDialog::exec() { setUserInputsEnabled(false); ui->progressBar->setVisible(true); + ui->copyCodeButton->setVisible(false); // Setup the login task and start it m_account = MinecraftAccount::createBlankMSA(); @@ -68,6 +70,8 @@ void MSALoginDialog::externalLoginTick() { void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) { + ui->copyCodeButton->setVisible(true); + m_externalLoginElapsed = 0; m_externalLoginTimeout = expiresIn; @@ -81,9 +85,12 @@ void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& QString urlString = uri.toString(); QString linkString = QString("%2").arg(urlString, urlString); ui->label->setText(tr("

Please open up %1 in a browser and put in the code %2 to proceed with login.

").arg(linkString, code)); + + m_code = code; } void MSALoginDialog::hideVerificationUriAndCode() { + ui->copyCodeButton->setVisible(false); m_externalLoginTimer.stop(); } @@ -139,3 +146,8 @@ MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg) } return 0; } + +void MSALoginDialog::on_copyCodeButton_clicked() +{ + QApplication::clipboard()->setText(m_code); +} \ No newline at end of file diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index 4cf146ab..963f550b 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -49,6 +49,7 @@ slots: void onTaskProgress(qint64 current, qint64 total); void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); void hideVerificationUriAndCode(); + void on_copyCodeButton_clicked(); void externalLoginTick(); @@ -57,6 +58,7 @@ private: MinecraftAccountPtr m_account; shared_qobject_ptr m_loginTask; QTimer m_externalLoginTimer; + QString m_code; int m_externalLoginElapsed = 0; int m_externalLoginTimeout = 0; }; diff --git a/launcher/ui/dialogs/MSALoginDialog.ui b/launcher/ui/dialogs/MSALoginDialog.ui index 78cbfb26..0921e38a 100644 --- a/launcher/ui/dialogs/MSALoginDialog.ui +++ b/launcher/ui/dialogs/MSALoginDialog.ui @@ -49,14 +49,25 @@ aaaaa - - - Qt::Horizontal - - - QDialogButtonBox::Cancel - - + + + + + Copy Code + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + diff --git a/launcher/ui/dialogs/ModrinthExportDialog.cpp b/launcher/ui/dialogs/ModrinthExportDialog.cpp new file mode 100644 index 00000000..1762f9a6 --- /dev/null +++ b/launcher/ui/dialogs/ModrinthExportDialog.cpp @@ -0,0 +1,142 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include +#include "ModrinthExportDialog.h" +#include "ui_ModrinthExportDialog.h" +#include "BaseInstance.h" +#include "modplatform/modrinth/ModrinthInstanceExportTask.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ProgressDialog.h" +#include "CustomMessageBox.h" + + +ModrinthExportDialog::ModrinthExportDialog(InstancePtr instance, QWidget *parent) : + QDialog(parent), ui(new Ui::ModrinthExportDialog), m_instance(instance) +{ + ui->setupUi(this); + ui->name->setText(m_instance->name()); + ui->version->setText("1.0"); +} + +void ModrinthExportDialog::updateDialogState() +{ + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled( + !ui->name->text().isEmpty() + && !ui->version->text().isEmpty() + && ui->file->text().endsWith(".mrpack") + && ( + !ui->includeDatapacks->isChecked() + || (!ui->datapacksPath->text().isEmpty() && QDir(m_instance->gameRoot() + "/" + ui->datapacksPath->text()).exists()) + ) + ); +} + +void ModrinthExportDialog::on_fileBrowseButton_clicked() +{ + QFileDialog dialog(this, tr("Select modpack file"), QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + dialog.setDefaultSuffix("mrpack"); + dialog.setNameFilter("Modrinth modpacks (*.mrpack)"); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.selectFile(ui->name->text() + ".mrpack"); + + if (dialog.exec()) { + ui->file->setText(dialog.selectedFiles().at(0)); + } + + updateDialogState(); +} + +void ModrinthExportDialog::on_datapackPathBrowse_clicked() +{ + QFileDialog dialog(this, tr("Select global datapacks folder"), m_instance->gameRoot()); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + dialog.setFileMode(QFileDialog::DirectoryOnly); + + if (dialog.exec()) { + ui->datapacksPath->setText(QDir(m_instance->gameRoot()).relativeFilePath(dialog.selectedFiles().at(0))); + } + + updateDialogState(); +} + +void ModrinthExportDialog::accept() +{ + Modrinth::ExportSettings settings; + + settings.name = ui->name->text(); + settings.version = ui->version->text(); + settings.description = ui->description->text(); + + settings.includeGameConfig = ui->includeGameConfig->isChecked(); + settings.includeModConfigs = ui->includeModConfigs->isChecked(); + settings.includeResourcePacks = ui->includeResourcePacks->isChecked(); + settings.includeShaderPacks = ui->includeShaderPacks->isChecked(); + + if (ui->includeDatapacks->isChecked()) { + settings.datapacksPath = ui->datapacksPath->text(); + } + + MinecraftInstancePtr minecraftInstance = std::dynamic_pointer_cast(m_instance); + minecraftInstance->getPackProfile()->reload(Net::Mode::Offline); + + for (int i = 0; i < minecraftInstance->getPackProfile()->rowCount(); i++) { + auto component = minecraftInstance->getPackProfile()->getComponent(i); + if (component->isCustom()) { + CustomMessageBox::selectable( + this, + tr("Warning"), + tr("Instance contains a custom component: %1\nThis cannot be exported to a Modrinth pack; the exported pack may not work correctly!") + .arg(component->getName()), + QMessageBox::Warning + )->exec(); + } + } + + settings.gameVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.minecraft"); + settings.forgeVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.minecraftforge"); + settings.fabricVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader"); + settings.quiltVersion = minecraftInstance->getPackProfile()->getComponentVersion("org.quiltmc.quilt-loader"); + + settings.exportPath = ui->file->text(); + + auto *task = new Modrinth::InstanceExportTask(m_instance, settings); + + connect(task, &Task::failed, [this](QString reason) + { + QString text; + if (reason.length() > 1000) { + text = reason.left(1000) + "..."; + } else { + text = reason; + } + CustomMessageBox::selectable(parentWidget(), tr("Error"), text, QMessageBox::Critical)->show(); + }); + connect(task, &Task::succeeded, [this, task]() + { + QStringList warnings = task->warnings(); + if(warnings.count()) + { + CustomMessageBox::selectable(parentWidget(), tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + + QDialog::accept(); +} + +ModrinthExportDialog::~ModrinthExportDialog() +{ + delete ui; +} diff --git a/launcher/ui/dialogs/ModrinthExportDialog.h b/launcher/ui/dialogs/ModrinthExportDialog.h new file mode 100644 index 00000000..08a244c9 --- /dev/null +++ b/launcher/ui/dialogs/ModrinthExportDialog.h @@ -0,0 +1,38 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include "ExportInstanceDialog.h" + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class ModrinthExportDialog; +} +QT_END_NAMESPACE + +class ModrinthExportDialog : public QDialog +{ +Q_OBJECT + +public: + explicit ModrinthExportDialog(InstancePtr instance, QWidget *parent = nullptr); + + ~ModrinthExportDialog() override; + +private slots: + void on_fileBrowseButton_clicked(); + void on_datapackPathBrowse_clicked(); + void accept() override; + void updateDialogState(); + +private: + Ui::ModrinthExportDialog *ui; + InstancePtr m_instance; +}; \ No newline at end of file diff --git a/launcher/ui/dialogs/ModrinthExportDialog.ui b/launcher/ui/dialogs/ModrinthExportDialog.ui new file mode 100644 index 00000000..c386f881 --- /dev/null +++ b/launcher/ui/dialogs/ModrinthExportDialog.ui @@ -0,0 +1,333 @@ + + + ModrinthExportDialog + + + + 0 + 0 + 835 + 559 + + + + + 0 + 0 + + + + ModrinthExportDialog + + + + + 10 + 10 + 821 + 541 + + + + + + + + 16777215 + 25 + + + + Export Modrinth modpack + + + + + + + + 16777215 + 200 + + + + Metadata + + + + + 10 + 30 + 801 + 151 + + + + + + + + + Name + + + + + + + + + + Version + + + + + + + Description + + + + + + + + + + + + + + + + + + + Export Options + + + + + 9 + 29 + 801 + 221 + + + + + + + QLayout::SetFixedSize + + + + + File + + + + + + + + + + Browse... + + + + + + + + + Include Minecraft config + + + true + + + + + + + Include mod configs + + + true + + + + + + + Include resource packs + + + + + + + Include shader packs + + + + + + + + + Use this if your modpack contains a mod which adds global datapacks. + + + Include global datapacks folder: + + + + + + + + + + Browse... + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + ModrinthExportDialog + accept() + + + 340 + 532 + + + 338 + 279 + + + + + buttonBox + rejected() + ModrinthExportDialog + reject() + + + 340 + 532 + + + 338 + 279 + + + + + name + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 395 + 90 + + + 339 + 279 + + + + + version + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 395 + 129 + + + 339 + 279 + + + + + file + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 309 + 329 + + + 339 + 279 + + + + + datapacksPath + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 532 + 472 + + + 417 + 279 + + + + + includeDatapacks + stateChanged(int) + ModrinthExportDialog + updateDialogState() + + + 183 + 472 + + + 417 + 279 + + + + + + updateDialogState() + + diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp index c2189c2b..f949ebe6 100644 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ b/launcher/ui/dialogs/UpdateDialog.cpp @@ -11,14 +11,13 @@ UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog) { ui->setupUi(this); - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); if(hasUpdate) { - ui->label->setText(tr("A new %1 update is available!").arg(channel)); + ui->label->setText(tr("A new update is available!")); } else { - ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel)); + ui->label->setText(tr("No updates found. You are running the latest version.")); ui->btnUpdateNow->setHidden(true); ui->btnUpdateLater->setText(tr("Close")); } @@ -33,19 +32,10 @@ UpdateDialog::~UpdateDialog() void UpdateDialog::loadChangelog() { - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); dljob = new NetJob("Changelog", APPLICATION->network()); QString url; - if(channel == "stable") - { - url = QString("https://raw.githubusercontent.com/MultiMC/Launcher/%1/changelog.md").arg(channel); - m_changelogType = CHANGELOG_MARKDOWN; - } - else - { - url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel); - m_changelogType = CHANGELOG_COMMITS; - } + url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...develop").arg(BuildConfig.GIT_COMMIT); + m_changelogType = CHANGELOG_COMMITS; dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded); connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed); @@ -65,7 +55,6 @@ QString reprocessMarkdown(QByteArray markdown) QString reprocessCommits(QByteArray json) { - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); try { QString result; @@ -119,7 +108,7 @@ QString reprocessCommits(QByteArray json) if(status == "identical") { - return QObject::tr("

There are no code changes between your current version and latest %1.

").arg(channel); + return QObject::tr("

There are no code changes between your current version and the latest.

"); } else if(status == "ahead") { diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 2eb73e44..1f986c19 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -56,23 +56,12 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch m_languageModel = APPLICATION->translations(); loadSettings(); - if(BuildConfig.UPDATER_ENABLED) - { - QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList); - - if (APPLICATION->updateChecker()->hasChannels()) - { - refreshUpdateChannelList(); - } - else - { - APPLICATION->updateChecker()->updateChanList(false); - } - } - else + // Updater + if(!BuildConfig.UPDATER_ENABLED) { ui->updateSettingsBox->setHidden(true); } + // Analytics if(BuildConfig.ANALYTICS_ID.isEmpty()) { @@ -163,78 +152,6 @@ void LauncherPage::on_migrateDataFolderMacBtn_clicked() qApp->quit(); } -void LauncherPage::refreshUpdateChannelList() -{ - // Stop listening for selection changes. It's going to change a lot while we update it and - // we don't need to update the - // description label constantly. - QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(updateChannelSelectionChanged(int))); - - QList channelList = APPLICATION->updateChecker()->getChannelList(); - ui->updateChannelComboBox->clear(); - int selection = -1; - for (int i = 0; i < channelList.count(); i++) - { - UpdateChecker::ChannelListEntry entry = channelList.at(i); - - // When it comes to selection, we'll rely on the indexes of a channel entry being the - // same in the - // combo box as it is in the update checker's channel list. - // This probably isn't very safe, but the channel list doesn't change often enough (or - // at all) for - // this to be a big deal. Hope it doesn't break... - ui->updateChannelComboBox->addItem(entry.name); - - // If the update channel we just added was the selected one, set the current index in - // the combo box to it. - if (entry.id == m_currentUpdateChannel) - { - qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel; - selection = i; - } - } - - ui->updateChannelComboBox->setCurrentIndex(selection); - - // Start listening for selection changes again and update the description label. - QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(updateChannelSelectionChanged(int))); - refreshUpdateChannelDesc(); - - // Now that we've updated the channel list, we can enable the combo box. - // It starts off disabled so that if the channel list hasn't been loaded, it will be - // disabled. - ui->updateChannelComboBox->setEnabled(true); -} - -void LauncherPage::updateChannelSelectionChanged(int index) -{ - refreshUpdateChannelDesc(); -} - -void LauncherPage::refreshUpdateChannelDesc() -{ - // Get the channel list. - QList channelList = APPLICATION->updateChecker()->getChannelList(); - int selectedIndex = ui->updateChannelComboBox->currentIndex(); - if (selectedIndex < 0) - { - return; - } - if (selectedIndex < channelList.count()) - { - // Find the channel list entry with the given index. - UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex); - - // Set the description text. - ui->updateChannelDescLabel->setText(selected.description); - - // Set the currently selected channel ID. - m_currentUpdateChannel = selected.id; - } -} - void LauncherPage::applySettings() { auto s = APPLICATION->settings(); @@ -246,7 +163,6 @@ void LauncherPage::applySettings() // Updates s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); - s->set("UpdateChannel", m_currentUpdateChannel); auto original = s->get("IconTheme").toString(); //FIXME: make generic switch (ui->themeComboBox->currentIndex()) @@ -333,7 +249,6 @@ void LauncherPage::loadSettings() auto s = APPLICATION->settings(); // Updates ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); - m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); if (theme == "pe_dark") diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 4d0cf3c9..d5ea2353 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -69,31 +69,14 @@ slots: void on_iconsDirBrowseBtn_clicked(); void on_migrateDataFolderMacBtn_clicked(); - /*! - * Updates the list of update channels in the combo box. - */ - void refreshUpdateChannelList(); - - /*! - * Updates the channel description label. - */ - void refreshUpdateChannelDesc(); - /*! * Updates the font preview */ void refreshFontPreview(); - void updateChannelSelectionChanged(int index); - private: Ui::LauncherPage *ui; - /*! - * Stores the currently selected update channel. - */ - QString m_currentUpdateChannel; - // default format for the font preview... QTextCharFormat *defaultFormat; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 62a66d73..d1728fed 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -58,33 +58,6 @@ - - - - Up&date Channel: - - - updateChannelComboBox - - - - - - - false - - - - - - - No channel selected. - - - true - - - @@ -555,7 +528,6 @@ tabWidget autoUpdateCheckBox - updateChannelComboBox instDirTextBox instDirBrowseBtn modsDirTextBox diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index c763f8ac..8491e988 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2021 MultiMC Contributors +/* Copyright 2013-2022 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,6 +71,7 @@ void MinecraftPage::applySettings() s->set("ShowGameTime", ui->showGameTime->isChecked()); s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); s->set("RecordGameTime", ui->recordGameTime->isChecked()); + s->set("ShowGameTimeHours", ui->showGameTimeHours->isChecked()); } void MinecraftPage::loadSettings() @@ -88,4 +89,5 @@ void MinecraftPage::loadSettings() ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); + ui->showGameTimeHours->setChecked(s->get("ShowGameTimeHours").toBool()); } diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 857b8cfb..7a5137d8 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -161,6 +161,13 @@ + + + + Show time spent playing in hours only + + + diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 4011d88c..e70117eb 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -314,6 +314,20 @@ void ScreenshotsPage::on_actionUpload_triggered() if (selection.isEmpty()) return; + auto uploadText = tr("Upload screenshot to imgur.com?"); + if (selection.size() > 1) + uploadText = tr("Upload %1 screenshots to imgur.com?").arg(selection.size()); + + auto response = CustomMessageBox::selectable( + this, + tr("Upload?"), + uploadText, + QMessageBox::Question, + QMessageBox::Yes | QMessageBox::No + )->exec(); + if (response == QMessageBox::No) + return; + QList uploaded; auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); if(selection.size() < 2) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index ac3869dc..c0aaf450 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2021 Jamie Mansfield + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ #include "AtlOptionalModDialog.h" #include "ui_AtlOptionalModDialog.h" -AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector mods) - : QAbstractListModel(parent), m_mods(mods) { +#include + +AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector mods) + : QAbstractListModel(parent), m_version(version), m_mods(mods) { // fill mod index for (int i = 0; i < m_mods.size(); i++) { @@ -71,6 +73,11 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const return mod.description; } } + else if (role == Qt::ForegroundRole) { + if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) { + return QColor(QString("#%1").arg(m_version.colours[mod.colour])); + } + } else if (role == Qt::CheckStateRole) { if (index.column() == EnabledColumn) { return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; @@ -134,7 +141,21 @@ void AtlOptionalModListModel::clearAll() { } void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { - setMod(mod, index, !m_selection[mod.name]); + auto enable = !m_selection[mod.name]; + + // If there is a warning for the mod, display that first (if we would be enabling the mod) + if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) { + auto message = QString("%1

%2") + .arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?")); + + // fixme: avoid casting here + auto result = QMessageBox::warning((QWidget*) this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No); + if (result != QMessageBox::Yes) { + return; + } + } + + setMod(mod, index, enable); } void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { @@ -199,11 +220,11 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool } -AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector mods) +AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector mods) : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { ui->setupUi(this); - listModel = new AtlOptionalModListModel(this, mods); + listModel = new AtlOptionalModListModel(this, version, mods); ui->treeView->setModel(listModel); ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 9832014c..26a2fdfa 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Jamie Mansfield + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ public: DescriptionColumn, }; - AtlOptionalModListModel(QWidget *parent, QVector mods); + AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector mods); QVector getResult(); @@ -58,7 +58,9 @@ private: void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); private: + ATLauncher::PackVersion m_version; QVector m_mods; + QMap m_selection; QMap m_index; QMap> m_dependants; @@ -68,7 +70,7 @@ class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget *parent, QVector mods); + AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector mods); ~AtlOptionalModDialog() override; QVector getResult() { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index af0cc8d6..8b5a3187 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020-2022 Jamie Mansfield * Copyright 2021 Philip T * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +26,8 @@ #include +#include + AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) { @@ -89,7 +91,7 @@ void AtlPage::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion)); auto editedLogoName = selected.safeName; auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) @@ -145,8 +147,8 @@ void AtlPage::onVersionSelectionChanged(QString data) suggestCurrent(); } -QVector AtlPage::chooseOptionalMods(QVector mods) { - AtlOptionalModDialog optionalModDialog(this, mods); +QVector AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) { + AtlOptionalModDialog optionalModDialog(this, version, mods); optionalModDialog.exec(); return optionalModDialog.getResult(); } @@ -186,3 +188,8 @@ QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVers vselect.exec(); return vselect.selectedVersion()->descriptor(); } + +void AtlPage::displayMessage(QString message) +{ + QMessageBox::information(this, tr("Installing"), message); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 5b3f2228..a7c56eb1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +64,8 @@ private: void suggestCurrent(); QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; - QVector chooseOptionalMods(QVector mods) override; + QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; + void displayMessage(QString message) override; private slots: void triggerSearch(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 50fd75e8..e92cda17 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -1,4 +1,5 @@ /* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ #include #include +#include namespace Technic { struct Modpack { @@ -36,6 +38,11 @@ struct Modpack { QString websiteUrl; QString author; QString description; + QString currentVersion; + + bool versionsLoaded = false; + QString recommended; + QVector versions; }; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 0bc0d498..b0128b98 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -1,4 +1,5 @@ /* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +16,7 @@ #include "TechnicModel.h" #include "Application.h" +#include "BuildConfig.h" #include "Json.h" #include @@ -94,13 +96,24 @@ void Technic::ListModel::performSearch() NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = "https://api.technicpack.net/trending?build=multimc"; + searchUrl = QString("%1trending?build=%2") + .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); + searchMode = List; } - else - { + else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=%2") + .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; + } + else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; + } + else { searchUrl = QString( - "https://api.technicpack.net/search?build=multimc&q=%1" - ).arg(currentSearchTerm); + "%1search?build=%2&q=%3" + ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); + searchMode = List; } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; @@ -125,26 +138,58 @@ void Technic::ListModel::searchRequestFinished() QList newList; try { auto root = Json::requireObject(doc); - auto objs = Json::requireArray(root, "modpacks"); - for (auto technicPack: objs) { - Modpack pack; - auto technicPackObject = Json::requireValueObject(technicPack); - pack.name = Json::requireString(technicPackObject, "name"); - pack.slug = Json::requireString(technicPackObject, "slug"); - if (pack.slug == "vanilla") - continue; - auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); - if(rawURL == "null") { - pack.logoUrl = "null"; - pack.logoName = "null"; + switch (searchMode) { + case List: { + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireValueObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + break; } - else { - pack.logoUrl = rawURL; - pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + case Single: { + if (root.contains("error")) { + // Invalid API url + break; + } + + Modpack pack; + pack.name = Json::requireString(root, "displayName"); + pack.slug = Json::requireString(root, "name"); + + if (root.contains("icon")) { + auto iconObj = Json::requireObject(root, "icon"); + auto iconUrl = Json::requireString(iconObj, "url"); + + pack.logoUrl = iconUrl; + pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + + pack.broken = false; + newList.append(pack); + break; } - pack.broken = false; - newList.append(pack); } } catch (const JSONValidationError &err) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index e80e6e7c..d2748811 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -1,4 +1,5 @@ /* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +64,10 @@ private: ResetRequested, Finished } searchState = None; + enum SearchMode { + List, + Single, + } searchMode = List; NetJob::Ptr jobPtr; QByteArray response; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 64054168..d05eadfa 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -1,4 +1,5 @@ /* Copyright 2013-2022 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +22,11 @@ #include "ui/dialogs/NewInstanceDialog.h" +#include "BuildConfig.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" +#include "modplatform/technic/SolderPackManifest.h" #include "Json.h" #include "Application.h" @@ -36,7 +39,9 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -74,13 +79,14 @@ void TechnicPage::triggerSearch() { void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) { + ui->versionSelectionBox->clear(); + if(!first.isValid()) { if(isOpened) { dialog->setSuggestedPack(); } - //ui->frame->clear(); return; } @@ -113,17 +119,19 @@ void TechnicPage::suggestCurrent() } NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr response = std::make_shared(); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); + QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] { + jobPtr.reset(); + if (current.slug != slug) { return; } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonObject obj = doc.object(); if(parse_error.error != QJsonParseError::NoError) { @@ -165,10 +173,14 @@ void TechnicPage::suggestCurrent() current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); current.metadataLoaded = true; + metadataLoaded(); }); - netJob->start(); + + jobPtr = netJob; + jobPtr->start(); } // expects current.metadataLoaded to be true @@ -178,26 +190,119 @@ void TechnicPage::metadataLoaded() QString name = current.name; if (current.websiteUrl.isEmpty()) - // This allows injecting HTML here. - text = name; + text = name.toHtmlEscaped(); else - // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML. - text = "" + name + ""; + text = "" + name.toHtmlEscaped() + ""; + if (!current.author.isEmpty()) { - // This allows injecting HTML here - text += tr(" by ") + current.author; + text += "
" + tr(" by ") + current.author.toHtmlEscaped(); } text += "

"; ui->packDescription->setHtml(text + current.description); + + // Strip trailing forward-slashes from Solder URL's + if (current.isSolder) { + while (current.url.endsWith('/')) current.url.chop(1); + } + + // Display versions from Solder + if (!current.isSolder) { + // If the pack isn't a Solder pack, it only has the single version + ui->versionSelectionBox->addItem(current.currentVersion); + } + else if (current.versionsLoaded) { + // reverse foreach, so that the newest versions are first + for (auto i = current.versions.size(); i--;) { + ui->versionSelectionBox->addItem(current.versions.at(i)); + } + ui->versionSelectionBox->setCurrentText(current.recommended); + } + else { + // For now, until the versions are pulled from the Solder instance, display the current + // version so we can display something quicker + ui->versionSelectionBox->addItem(current.currentVersion); + + auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto url = QString("%1/modpack/%2").arg(current.url, current.slug); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + + jobPtr = netJob; + jobPtr->start(); + } + + selectVersion(); +} + +void TechnicPage::selectVersion() { + if (!isOpened) { + return; + } + if (current.broken) { + dialog->setSuggestedPack(); + return; + } + if (!current.isSolder) { - dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { - while (current.url.endsWith('/')) current.url.chop(1); - dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, selectedVersion, current.minecraftVersion)); } } + +void TechnicPage::onSolderLoaded() { + jobPtr.reset(); + + auto fallback = [this]() { + current.versionsLoaded = true; + + current.versions.clear(); + current.versions.append(current.currentVersion); + }; + + current.versions.clear(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + fallback(); + return; + } + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError &err) { + qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); + fallback(); + return; + } + + current.versionsLoaded = true; + current.recommended = pack.recommended; + current.versions << pack.builds; + + // Finally, let's reload :) + ui->versionSelectionBox->clear(); + metadataLoaded(); +} + +void TechnicPage::onVersionSelectionChanged(QString data) { + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; + } + + selectedVersion = data; + selectVersion(); +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 21695dd0..920ca009 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +20,7 @@ #include "ui/pages/BasePage.h" #include +#include "net/NetJob.h" #include "tasks/Task.h" #include "TechnicData.h" @@ -65,14 +67,22 @@ public: private: void suggestCurrent(); void metadataLoaded(); + void selectVersion(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onSolderLoaded(); + void onVersionSelectionChanged(QString data); private: Ui::TechnicPage *ui = nullptr; NewInstanceDialog* dialog = nullptr; Technic::ListModel* model = nullptr; + Technic::Modpack current; + QString selectedVersion; + + NetJob::Ptr jobPtr; + QByteArray response; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index ab6b4255..aa2df370 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -10,52 +10,44 @@ 405 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Search and filter ... - - - - - - - Search - - - - - - - - - - 0 - + + + + + + - + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - - Qt::ScrollBarAlwaysOff + + + Qt::Horizontal + + QSizePolicy::Preferred + + + + 1 + 1 + + + + + + + + + + true @@ -67,14 +59,27 @@ + + + + + + + Search and filter ... + + + + + + + Search + + + - - searchEdit - searchButton - diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index efdb6093..c0505908 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -26,11 +26,11 @@ #include "BuildConfig.h" #include "sys.h" -UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel, int currentBuild) +UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QString channelUrl, int currentBuild) { m_network = nam; m_channelUrl = channelUrl; - m_currentChannel = currentChannel; + m_currentChannel = "develop"; m_currentBuild = currentBuild; } @@ -44,9 +44,10 @@ bool UpdateChecker::hasChannels() const return !m_channels.isEmpty(); } -void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) +void UpdateChecker::checkForUpdate(bool notifyNoUpdate) { qDebug() << "Checking for updates."; + QString updateChannel = "develop"; // If the channel list hasn't loaded yet, load it and defer checking for updates until // later. @@ -54,7 +55,6 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) { qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; m_checkUpdateWaiting = true; - m_deferredUpdateChannel = updateChannel; updateChanList(notifyNoUpdate); return; } @@ -67,13 +67,13 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) // Find the desired channel within the channel list and get its repo URL. If if cannot be // found, error. - QString stableUrl; + QString developUrl; m_newRepoUrl = ""; for (ChannelListEntry entry : m_channels) { qDebug() << "channelEntry = " << entry.id; - if(entry.id == "stable") { - stableUrl = entry.url; + if(entry.id == "develop") { + developUrl = entry.url; } if (entry.id == updateChannel) { m_newRepoUrl = entry.url; @@ -88,8 +88,8 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) qDebug() << "m_repoUrl = " << m_newRepoUrl; if (m_newRepoUrl.isEmpty()) { - qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; - m_newRepoUrl = stableUrl; + qWarning() << "m_repoUrl was empty. defaulting to 'develop': " << developUrl; + m_newRepoUrl = developUrl; } // If nothing applies, error @@ -255,7 +255,7 @@ void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate) // If we're waiting to check for updates, do that now. if (m_checkUpdateWaiting) { - checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate); + checkForUpdate(notifyNoUpdate); } emit channelListLoaded(); diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h index 13ee4efd..6fe41807 100644 --- a/launcher/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -23,8 +23,8 @@ class UpdateChecker : public QObject Q_OBJECT public: - UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel, int currentBuild); - void checkForUpdate(QString updateChannel, bool notifyNoUpdate); + UpdateChecker(shared_qobject_ptr nam, QString channelUrl, int currentBuild); + void checkForUpdate(bool notifyNoUpdate); /*! * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). @@ -107,11 +107,6 @@ private: */ bool m_checkUpdateWaiting = false; - /*! - * if m_checkUpdateWaiting, this is the last used update channel - */ - QString m_deferredUpdateChannel; - int m_currentBuild = -1; QString m_currentChannel; QString m_currentRepoUrl; diff --git a/launcher/updater/UpdateChecker_test.cpp b/launcher/updater/UpdateChecker_test.cpp index ec55a40e..845ed993 100644 --- a/launcher/updater/UpdateChecker_test.cpp +++ b/launcher/updater/UpdateChecker_test.cpp @@ -42,38 +42,32 @@ slots: void tst_ChannelListParsing_data() { - QTest::addColumn("channel"); QTest::addColumn("channelUrl"); QTest::addColumn("hasChannels"); QTest::addColumn("valid"); QTest::addColumn >("result"); QTest::newRow("garbage") - << QString() << findTestDataUrl("data/garbageChannels.json") << false << false << QList(); QTest::newRow("errors") - << QString() << findTestDataUrl("data/errorChannels.json") << false << true << QList(); QTest::newRow("no channels") - << QString() << findTestDataUrl("data/noChannels.json") << false << true << QList(); QTest::newRow("one channel") - << QString("develop") << findTestDataUrl("data/oneChannel.json") << true << true << (QList() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"}); QTest::newRow("several channels") - << QString("develop") << findTestDataUrl("data/channels.json") << true << true @@ -84,15 +78,13 @@ slots: } void tst_ChannelListParsing() { - - QFETCH(QString, channel); QFETCH(QString, channelUrl); QFETCH(bool, hasChannels); QFETCH(bool, valid); QFETCH(QList, result); shared_qobject_ptr nam = new QNetworkAccessManager(); - UpdateChecker checker(nam, channelUrl, channel, 0); + UpdateChecker checker(nam, channelUrl, 0); QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); QVERIFY(channelListLoadedSpy.isValid()); @@ -116,12 +108,11 @@ slots: void tst_UpdateChecking() { - QString channel = "develop"; QString channelUrl = findTestDataUrl("data/channels.json"); int currentBuild = 2; shared_qobject_ptr nam = new QNetworkAccessManager(); - UpdateChecker checker(nam, channelUrl, channel, currentBuild); + UpdateChecker checker(nam, channelUrl, currentBuild); QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(GoUpdate::Status))); QVERIFY(updateAvailableSpy.isValid()); @@ -133,7 +124,7 @@ slots: qDebug() << "CWD:" << QDir::current().absolutePath(); checker.m_channels[0].url = findTestDataUrl("data/"); - checker.checkForUpdate(channel, false); + checker.checkForUpdate(false); QVERIFY(updateAvailableSpy.wait()); diff --git a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java index d341cad7..f57ad636 100644 --- a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java @@ -1,4 +1,4 @@ -/* Copyright 2012-2021 MultiMC Contributors +/* Copyright 2012-2023 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,9 @@ public class OneSixLauncher implements Launcher private String windowTitle; private String windowParams; + private String instanceTitle; + private String instanceIconId; + // secondary parameters private int winSizeW; private int winSizeH; @@ -50,6 +53,7 @@ public class OneSixLauncher implements Launcher private String serverAddress; private String serverPort; + private boolean useQuickPlay; // the much abused system classloader, for convenience (for further abuse) private ClassLoader cl; @@ -68,8 +72,16 @@ public class OneSixLauncher implements Launcher windowTitle = params.firstSafe("windowTitle", "Minecraft"); windowParams = params.firstSafe("windowParams", "854x480"); + instanceTitle = params.firstSafe("instanceTitle", "Minecraft"); + instanceIconId = params.firstSafe("instanceIconId", "default"); + + // NOTE: this is included for the CraftPresence mod + System.setProperty("multimc.instance.title", instanceTitle); + System.setProperty("multimc.instance.icon", instanceIconId); + serverAddress = params.firstSafe("serverAddress", null); serverPort = params.firstSafe("serverPort", null); + useQuickPlay = params.firstSafe("useQuickPlay").startsWith("1"); cwd = System.getProperty("user.dir"); @@ -175,10 +187,18 @@ public class OneSixLauncher implements Launcher if (serverAddress != null) { - mcparams.add("--server"); - mcparams.add(serverAddress); - mcparams.add("--port"); - mcparams.add(serverPort); + if (useQuickPlay) + { + mcparams.add("--quickPlayMultiplayer"); + mcparams.add(serverAddress + ":" + serverPort); + } + else + { + mcparams.add("--server"); + mcparams.add(serverAddress); + mcparams.add("--port"); + mcparams.add(serverPort); + } } // Get the Minecraft Class.