diff options
author | Sefa Eyeoglu <contact@scrumplex.net> | 2022-07-10 19:38:30 +0200 |
---|---|---|
committer | Sefa Eyeoglu <contact@scrumplex.net> | 2022-07-10 19:38:30 +0200 |
commit | b3b76d5d56f9b6849464a6df2031058c98359fbc (patch) | |
tree | 211464fb189ef1202093f206d93f3b9f139fd284 /launcher | |
parent | 3bc02b9662b84c2ab86b5de1b08b4537177fde90 (diff) | |
parent | cd948dceaed4625e7a876f680d3dc028e6cfe6de (diff) | |
download | PrismLauncher-b3b76d5d56f9b6849464a6df2031058c98359fbc.tar.gz PrismLauncher-b3b76d5d56f9b6849464a6df2031058c98359fbc.tar.bz2 PrismLauncher-b3b76d5d56f9b6849464a6df2031058c98359fbc.zip |
Merge branch 'develop' into feature/sparkle-mac
# Conflicts:
# .github/workflows/build.yml
Diffstat (limited to 'launcher')
298 files changed, 10094 insertions, 3930 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 456ea02c..f8c93259 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,6 +37,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "net/PasteUpload.h" #include "ui/MainWindow.h" #include "ui/InstanceWindow.h" @@ -61,6 +63,7 @@ #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" +#include "ui/setupwizard/PasteWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" @@ -81,6 +84,7 @@ #include <QDebug> #include <QStyleFactory> #include <QWindow> +#include <QIcon> #include "InstanceList.h" @@ -96,7 +100,6 @@ #include "tools/JVisualVM.h" #include "tools/MCEditTool.h" -#include <xdgicon.h> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -221,7 +224,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); - setApplicationDisplayName(BuildConfig.LAUNCHER_DISPLAYNAME); + setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); setApplicationVersion(BuildConfig.printableVersionString()); setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); startTime = QDateTime::currentDateTime(); @@ -329,10 +332,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) FS::updateTimestamp(m_rootPath); #endif - -#ifdef LAUNCHER_JARS_LOCATION - m_jarsPath = TOSTRING(LAUNCHER_JARS_LOCATION); -#endif } QString adjustedBy; @@ -622,6 +621,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("JavaPath", ""); m_settings->registerSetting("JavaTimestamp", 0); m_settings->registerSetting("JavaArchitecture", ""); + m_settings->registerSetting("JavaRealArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); m_settings->registerSetting("JavaVendor", ""); m_settings->registerSetting("LastHostname", ""); @@ -633,6 +633,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("UseNativeGLFW", false); + // Peformance related options + m_settings->registerSetting("EnableFeralGamemode", false); + m_settings->registerSetting("EnableMangoHud", false); + m_settings->registerSetting("UseDiscreteGpu", false); + // Game time m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); @@ -641,6 +646,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); + // Minecraft mods + m_settings->registerSetting("ModMetadataDisabled", false); + // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); @@ -672,14 +680,41 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); - // pastebin URL - m_settings->registerSetting("PastebinURL", "https://0x0.st"); + // HACK: This code feels so stupid is there a less stupid way of doing this? + { + m_settings->registerSetting("PastebinURL", ""); + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); + m_settings->registerSetting("PastebinCustomAPIBase", ""); + + QString pastebinURL = m_settings->get("PastebinURL").toString(); + + bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; + if (!pastebinURL.isEmpty() && !userHadDefaultPastebin) + { + m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); + m_settings->set("PastebinCustomAPIBase", pastebinURL); + m_settings->reset("PastebinURL"); + } + + bool ok; + int pasteType = m_settings->get("PastebinType").toInt(&ok); + // If PastebinType is invalid then reset the related settings. + if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) + { + m_settings->reset("PastebinType"); + m_settings->reset("PastebinCustomAPIBase"); + } + } + // meta URL + m_settings->registerSetting("MetaURLOverride", ""); m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); + m_settings->registerSetting("CFKeyOverride", ""); + m_settings->registerSetting("UserAgentOverride", ""); // Init page provider { @@ -843,6 +878,12 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_mcedit.reset(new MCEditTool(m_settings)); } +#ifdef Q_OS_MACOS + connect(this, &Application::clickedOnDock, [this]() { + this->showMainWindow(); + }); +#endif + connect(this, &Application::aboutToQuit, [this](){ if(m_instances) { @@ -899,7 +940,8 @@ bool Application::createSetupWizard() return true; return false; }(); - bool wizardRequired = javaRequired || languageRequired; + bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; + bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired; if(wizardRequired) { @@ -913,6 +955,11 @@ bool Application::createSetupWizard() { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); } + + if (pasteInterventionRequired) + { + m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); + } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); return true; @@ -920,6 +967,21 @@ bool Application::createSetupWizard() return false; } +bool Application::event(QEvent* event) { +#ifdef Q_OS_MACOS + if (event->type() == QEvent::ApplicationStateChange) { + auto ev = static_cast<QApplicationStateChangeEvent*>(event); + + if (m_prevAppState == Qt::ApplicationActive + && ev->applicationState() == Qt::ApplicationActive) { + emit clickedOnDock(); + } + m_prevAppState = ev->applicationState(); + } +#endif + return QApplication::event(event); +} + void Application::setupWizardFinished(int status) { qDebug() << "Wizard result =" << status; @@ -1121,7 +1183,7 @@ void Application::setApplicationTheme(const QString& name, bool initial) void Application::setIconTheme(const QString& name) { - XdgIcon::setThemeName(name); + QIcon::setThemeName(name); } QIcon Application::getThemedIcon(const QString& name) @@ -1129,7 +1191,7 @@ QIcon Application::getThemedIcon(const QString& name) if(name == "logo") { return QIcon(":/org.polymc.PolyMC.svg"); } - return XdgIcon::fromTheme(name); + return QIcon::fromTheme(name); } bool Application::openJsonEditor(const QString &filename) @@ -1491,13 +1553,22 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex() return m_metadataIndex; } -QString Application::getJarsPath() +QString Application::getJarPath(QString jarFile) { - if(m_jarsPath.isEmpty()) + QStringList potentialPaths = { +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + FS::PathCombine(m_rootPath, "share/jars"), +#endif + FS::PathCombine(m_rootPath, "jars"), + FS::PathCombine(applicationDirPath(), "jars") + }; + for(QString p : potentialPaths) { - return FS::PathCombine(QCoreApplication::applicationDirPath(), "jars"); + QString jarPath = FS::PathCombine(p, jarFile); + if (QFileInfo(jarPath).isFile()) + return jarPath; } - return FS::PathCombine(m_rootPath, m_jarsPath); + return {}; } QString Application::getMSAClientID() @@ -1509,3 +1580,34 @@ QString Application::getMSAClientID() return BuildConfig.MSA_CLIENT_ID; } + +QString Application::getCurseKey() +{ + QString keyOverride = m_settings->get("CFKeyOverride").toString(); + if (!keyOverride.isEmpty()) { + return keyOverride; + } + + return BuildConfig.CURSEFORGE_API_KEY; +} + +QString Application::getUserAgent() +{ + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); + } + + return BuildConfig.USER_AGENT; +} + +QString Application::getUserAgentUncached() +{ + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + uaOverride += " (Uncached)"; + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); + } + + return BuildConfig.USER_AGENT_UNCACHED; +} diff --git a/launcher/Application.h b/launcher/Application.h index 172321c0..e3b4fced 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -94,6 +94,8 @@ public: Application(int &argc, char **argv); virtual ~Application(); + bool event(QEvent* event) override; + std::shared_ptr<SettingsObject> settings() const { return m_settings; } @@ -152,9 +154,16 @@ public: shared_qobject_ptr<Meta::Index> metadataIndex(); - QString getJarsPath(); + /*! + * Finds and returns the full path to a jar file. + * Returns a null-string if it could not be found. + */ + QString getJarPath(QString jarFile); QString getMSAClientID(); + QString getCurseKey(); + QString getUserAgent(); + QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString &root() { @@ -180,6 +189,10 @@ signals: void globalSettingsAboutToOpen(); void globalSettingsClosed(); +#ifdef Q_OS_MACOS + void clickedOnDock(); +#endif + public slots: bool launch( InstancePtr instance, @@ -229,7 +242,6 @@ private: std::shared_ptr<GenericPageProvider> m_globalSettingsProvider; std::map<QString, std::unique_ptr<ITheme>> m_themes; std::unique_ptr<MCEditTool> m_mcedit; - QString m_jarsPath; QSet<QString> m_features; QMap<QString, std::shared_ptr<BaseProfilerFactory>> m_profilers; @@ -237,6 +249,10 @@ private: QString m_rootPath; Status m_status = Application::StartingUp; +#ifdef Q_OS_MACOS + Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; +#endif + #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; diff --git a/launcher/ApplicationMessage.cpp b/launcher/ApplicationMessage.cpp index e22bf13c..ca276b89 100644 --- a/launcher/ApplicationMessage.cpp +++ b/launcher/ApplicationMessage.cpp @@ -1,11 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ApplicationMessage.h" #include <QJsonDocument> #include <QJsonObject> +#include "Json.h" void ApplicationMessage::parse(const QByteArray & input) { - auto doc = QJsonDocument::fromBinaryData(input); - auto root = doc.object(); + auto doc = Json::requireDocument(input, "ApplicationMessage"); + auto root = Json::requireObject(doc, "ApplicationMessage"); command = root.value("command").toString(); args.clear(); @@ -25,7 +61,5 @@ QByteArray ApplicationMessage::serialize() { } root.insert("args", outArgs); - QJsonDocument out; - out.setObject(root); - return out.toBinaryData(); + return Json::toText(root); } diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 2fb31d94..5a84a931 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +39,7 @@ #include <QFileInfo> #include <QDir> #include <QDebug> +#include <QRegularExpression> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -59,7 +61,11 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); - m_settings->registerSetting("InstanceType", "OneSix"); + + // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of + // a locally stored instance + if (!m_settings->getSetting("InstanceType")) + m_settings->registerSetting("InstanceType", ""); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); @@ -76,6 +82,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() @@ -93,6 +107,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"); @@ -282,7 +336,7 @@ QString BaseInstance::name() const QString BaseInstance::windowTitle() const { - return BuildConfig.LAUNCHER_NAME + ": " + name().replace(QRegExp("[ \n\r\t]+"), " "); + return BuildConfig.LAUNCHER_NAME + ": " + name().replace(QRegularExpression("\\s+"), " "); } // FIXME: why is this here? move it to MinecraftInstance!!! diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index c973fcd4..2a94dcc6 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -139,6 +140,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) { @@ -179,6 +188,7 @@ public: * Create envrironment variables for running the instance */ virtual QProcessEnvironment createEnvironment() = 0; + virtual QProcessEnvironment createLaunchEnvironment() = 0; /*! * Returns a matcher that can maps relative paths within the instance to whether they are 'log files' diff --git a/launcher/BaseVersionList.cpp b/launcher/BaseVersionList.cpp index aa9cb6cf..b4a7d6dd 100644 --- a/launcher/BaseVersionList.cpp +++ b/launcher/BaseVersionList.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "BaseVersionList.h" @@ -51,7 +71,7 @@ QVariant BaseVersionList::data(const QModelIndex &index, int role) const switch (role) { case VersionPointerRole: - return qVariantFromValue(version); + return QVariant::fromValue(version); case VersionRole: return version->name(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0be10682..ecdeaac0 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -4,8 +4,6 @@ project(application) ######## Sources and headers ######## -include (UnitTest) - set(CORE_SOURCES # LOGIC - Base classes and infrastructure BaseInstaller.h @@ -90,16 +88,11 @@ set(CORE_SOURCES MMCTime.cpp ) -add_unit_test(FileSystem - SOURCES FileSystem_test.cpp - LIBS Launcher_logic - DATA testdata - ) +ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME FileSystem) # TODO: needs testdata -add_unit_test(GZip - SOURCES GZip_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME GZip) set(PATHMATCHER_SOURCES # Path matchers @@ -128,6 +121,8 @@ set(NET_SOURCES net/PasteUpload.h net/Sink.h net/Validator.h + net/Upload.cpp + net/Upload.h ) # Game launch logic @@ -170,18 +165,6 @@ set(MAC_UPDATE_SOURCES updater/MacSparkleUpdater.mm ) -add_unit_test(UpdateChecker - SOURCES updater/UpdateChecker_test.cpp - LIBS Launcher_logic - DATA updater/testdata - ) - -add_unit_test(DownloadTask - SOURCES updater/DownloadTask_test.cpp - LIBS Launcher_logic - DATA updater/testdata - ) - # Backend for the news bar... there's usually no news. set(NEWS_SOURCES # News System @@ -328,19 +311,22 @@ set(MINECRAFT_SOURCES minecraft/WorldList.h minecraft/WorldList.cpp + minecraft/mod/MetadataHandler.h minecraft/mod/Mod.h minecraft/mod/Mod.cpp minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp - minecraft/mod/ModFolderLoadTask.h - minecraft/mod/ModFolderLoadTask.cpp - minecraft/mod/LocalModParseTask.h - minecraft/mod/LocalModParseTask.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/tasks/ModFolderLoadTask.h + minecraft/mod/tasks/ModFolderLoadTask.cpp + minecraft/mod/tasks/LocalModParseTask.h + minecraft/mod/tasks/LocalModParseTask.cpp + minecraft/mod/tasks/LocalModUpdateTask.h + minecraft/mod/tasks/LocalModUpdateTask.cpp # Assets minecraft/AssetsUtils.h @@ -358,10 +344,8 @@ set(MINECRAFT_SOURCES mojang/PackageManifest.cpp minecraft/Agent.h) -add_unit_test(GradleSpecifier - SOURCES minecraft/GradleSpecifier_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(minecraft/GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME GradleSpecifier) if(BUILD_TESTING) add_executable(PackageManifest @@ -369,7 +353,7 @@ if(BUILD_TESTING) ) target_link_libraries(PackageManifest Launcher_logic - Qt5::Test + Qt${QT_VERSION_MAJOR}::Test ) target_include_directories(PackageManifest PRIVATE ../cmake/UnitTest/ @@ -381,28 +365,20 @@ if(BUILD_TESTING) ) endif() -add_unit_test(MojangVersionFormat - SOURCES minecraft/MojangVersionFormat_test.cpp - LIBS Launcher_logic - DATA minecraft/testdata - ) +# TODO: needs minecraft/testdata +ecm_add_test(minecraft/MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME MojangVersionFormat) -add_unit_test(Library - SOURCES minecraft/Library_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Library) # FIXME: shares data with FileSystem test -add_unit_test(ModFolderModel - SOURCES minecraft/mod/ModFolderModel_test.cpp - DATA testdata - LIBS Launcher_logic - ) +# TODO: needs testdata +ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ModFolderModel) -add_unit_test(ParseUtils - SOURCES minecraft/ParseUtils_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ParseUtils) # the screenshots feature set(SCREENSHOTS_SOURCES @@ -417,14 +393,14 @@ set(TASKS_SOURCES # Tasks tasks/Task.h tasks/Task.cpp + tasks/ConcurrentTask.h + tasks/ConcurrentTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp ) -add_unit_test(Task - SOURCES tasks/Task_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Task) set(SETTINGS_SOURCES # Settings @@ -442,10 +418,8 @@ set(SETTINGS_SOURCES settings/SettingsObject.h ) -add_unit_test(INIFile - SOURCES settings/INIFile_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(settings/INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME INIFile) set(JAVA_SOURCES java/JavaChecker.h @@ -462,10 +436,8 @@ set(JAVA_SOURCES java/JavaVersion.cpp ) -add_unit_test(JavaVersion - SOURCES java/JavaVersion_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(java/JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME JavaVersion) set(TRANSLATIONS_SOURCES translations/TranslationsModel.h @@ -503,6 +475,9 @@ set(META_SOURCES ) set(API_SOURCES + modplatform/ModIndex.h + modplatform/ModIndex.cpp + modplatform/ModAPI.h modplatform/flame/FlameAPI.h @@ -549,6 +524,15 @@ set(MODPACKSCH_SOURCES modplatform/modpacksch/FTBPackManifest.cpp ) +set(PACKWIZ_SOURCES + modplatform/packwiz/Packwiz.h + modplatform/packwiz/Packwiz.cpp +) + +# TODO: needs modplatform/packwiz/testdata +ecm_add_test(modplatform/packwiz/Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Packwiz) + set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.cpp @@ -571,10 +555,8 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) -add_unit_test(Index - SOURCES meta/Index_test.cpp - LIBS Launcher_logic - ) +ecm_add_test(meta/Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Index) ################################ COMPILE ################################ @@ -602,6 +584,7 @@ set(LOGIC_SOURCES ${FLAME_SOURCES} ${MODRINTH_SOURCES} ${MODPACKSCH_SOURCES} + ${PACKWIZ_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} ) @@ -671,6 +654,8 @@ SET(LAUNCHER_SOURCES ui/setupwizard/JavaWizardPage.h ui/setupwizard/LanguageWizardPage.cpp ui/setupwizard/LanguageWizardPage.h + ui/setupwizard/PasteWizardPage.cpp + ui/setupwizard/PasteWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp @@ -703,6 +688,8 @@ SET(LAUNCHER_SOURCES ui/pages/BasePageProvider.h # GUI - instance pages + ui/pages/instance/ExternalResourcesPage.cpp + ui/pages/instance/ExternalResourcesPage.h ui/pages/instance/GameOptionsPage.cpp ui/pages/instance/GameOptionsPage.h ui/pages/instance/VersionPage.cpp @@ -831,6 +818,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp ui/dialogs/NewInstanceDialog.h + ui/dialogs/NewsDialog.cpp + ui/dialogs/NewsDialog.h ui/pagedialog/PageDialog.cpp ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp @@ -845,6 +834,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ModDownloadDialog.h + ui/dialogs/ScrollMessageBox.cpp + ui/dialogs/ScrollMessageBox.h # GUI - widgets ui/widgets/Common.cpp @@ -899,7 +890,8 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) -qt5_wrap_ui(LAUNCHER_UI +qt_wrap_ui(LAUNCHER_UI + ui/setupwizard/PasteWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui @@ -907,7 +899,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/global/ProxyPage.ui ui/pages/global/MinecraftPage.ui ui/pages/global/ExternalToolsPage.ui - ui/pages/instance/ModFolderPage.ui + ui/pages/instance/ExternalResourcesPage.ui ui/pages/instance/NotesPage.ui ui/pages/instance/LogPage.ui ui/pages/instance/ServersPage.ui @@ -937,6 +929,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/NewInstanceDialog.ui ui/dialogs/UpdateDialog.ui ui/dialogs/NewComponentDialog.ui + ui/dialogs/NewsDialog.ui ui/dialogs/ProfileSelectDialog.ui ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui @@ -947,9 +940,10 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui + ui/dialogs/ScrollMessageBox.ui ) -qt5_add_resources(LAUNCHER_RESOURCES +qt_add_resources(LAUNCHER_RESOURCES resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc resources/pe_dark/pe_dark.qrc @@ -965,7 +959,7 @@ qt5_add_resources(LAUNCHER_RESOURCES ######## Windows resource files ######## if(WIN32) - set(LAUNCHER_RCS ../${Launcher_Branding_WindowsRC}) + set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() # Add executable @@ -979,16 +973,25 @@ target_link_libraries(Launcher_logic tomlc99 BuildConfig Katabasis + Qt${QT_VERSION_MAJOR}::Widgets ) + +if (UNIX AND NOT CYGWIN AND NOT APPLE) + target_link_libraries(Launcher_logic + gamemode + ) +endif() + target_link_libraries(Launcher_logic - Qt5::Core - Qt5::Xml - Qt5::Network - Qt5::Concurrent - Qt5::Gui + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Xml + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Concurrent + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets + ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic - Launcher_iconfix QuaZip::QuaZip hoedown LocalPeer @@ -1082,6 +1085,14 @@ if(INSTALL_BUNDLE STREQUAL "full") COMPONENT Runtime ) endif() + # TLS plugins (Qt 6 only) + if(EXISTS "${QT_PLUGINS_DIR}/tls") + install( + DIRECTORY "${QT_PLUGINS_DIR}/tls" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + endif() else() # Image formats install( @@ -1124,6 +1135,16 @@ if(INSTALL_BUNDLE STREQUAL "full") REGEX "\\.dSYM" EXCLUDE ) endif() + # TLS plugins (Qt 6 only) + if(EXISTS "${QT_PLUGINS_DIR}/tls") + install( + DIRECTORY "${QT_PLUGINS_DIR}/tls" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() endif() configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" diff --git a/launcher/Commandline.cpp b/launcher/Commandline.cpp index 2c0fde64..8e7356bb 100644 --- a/launcher/Commandline.cpp +++ b/launcher/Commandline.cpp @@ -1,18 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. * - * Authors: Orochimarufan <orochimarufan.x3@gmail.com> + * This file incorporates work covered by the following copyright and + * permission notice: * - * 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 + * Copyright 2013-2021 MultiMC Contributors * - * http://www.apache.org/licenses/LICENSE-2.0 + * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * - * 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. + * 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 "Commandline.h" @@ -47,7 +67,7 @@ QStringList splitArgs(QString args) if (cchar == '\\') escape = true; else if (cchar == inquotes) - inquotes = 0; + inquotes = QChar::Null; else current += cchar; // otherwise @@ -480,4 +500,4 @@ void Parser::getPrefix(QString &opt, QString &flag) ParsingError::ParsingError(const QString &what) : std::runtime_error(what.toStdString()) { } -}
\ No newline at end of file +} diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 6de20de6..ebb4460d 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -1,4 +1,37 @@ -// Licensed under the Apache-2.0 license. See README.md for details. +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "FileSystem.h" @@ -346,7 +379,7 @@ bool checkProblemticPathJava(QDir folder) } // Win32 crap -#if defined Q_OS_WIN +#ifdef Q_OS_WIN bool called_coinit = false; @@ -366,7 +399,7 @@ HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args) } } - IShellLink *link; + IShellLinkA *link; hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID *)&link); @@ -454,4 +487,47 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na return false; #endif } + +QStringList listFolderPaths(QDir root) +{ + auto createAbsPath = [](QFileInfo const& entry) { return FS::PathCombine(entry.path(), entry.fileName()); }; + + QStringList entries; + + root.refresh(); + for (auto entry : root.entryInfoList(QDir::Filter::Files)) { + entries.append(createAbsPath(entry)); + } + + for (auto entry : root.entryInfoList(QDir::Filter::AllDirs | QDir::Filter::NoDotAndDotDot)) { + entries.append(listFolderPaths(createAbsPath(entry))); + } + + return entries; +} + +bool overrideFolder(QString overwritten_path, QString override_path) +{ + if (!FS::ensureFolderPathExists(overwritten_path)) + return false; + + QStringList paths_to_override; + QDir root_override (override_path); + for (auto file : listFolderPaths(root_override)) { + QString destination = file; + destination.replace(override_path, overwritten_path); + + qDebug() << QString("Applying override %1 in %2").arg(file, destination); + + if (QFile::exists(destination)) + QFile::remove(destination); + if (!QFile::rename(file, destination)) { + qCritical() << QString("Failed to apply override from %1 to %2").arg(file, destination); + return false; + } + } + + return true; +} + } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 8f6e8b48..fd305b01 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -1,4 +1,37 @@ -// Licensed under the Apache-2.0 license. See README.md for details. +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #pragma once @@ -49,8 +82,8 @@ class copy public: copy(const QString & src, const QString & dst) { - m_src = src; - m_dst = dst; + m_src.setPath(src); + m_dst.setPath(dst); } copy & followSymlinks(const bool follow) { @@ -120,8 +153,7 @@ bool checkProblemticPathJava(QDir folder); // Get the Directory representing the User's Desktop QString getDesktopDir(); -// Create a shortcut at *location*, pointing to *dest* called with the arguments *args* -// call it *name* and assign it the icon *icon* -// return true if operation succeeded -bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation); +// Overrides one folder with the contents of another, preserving items exclusive to the first folder +// Equivalent to doing QDir::rename, but allowing for overrides +bool overrideFolder(QString overwritten_path, QString override_path); } diff --git a/launcher/FileSystem_test.cpp b/launcher/FileSystem_test.cpp index 90ddc499..99ae9269 100644 --- a/launcher/FileSystem_test.cpp +++ b/launcher/FileSystem_test.cpp @@ -1,7 +1,6 @@ #include <QTest> #include <QTemporaryDir> #include <QStandardPaths> -#include "TestUtil.h" #include "FileSystem.h" @@ -81,7 +80,7 @@ slots: void test_copy() { - QString folder = QFINDTESTDATA("data/test_folder"); + QString folder = QFINDTESTDATA("testdata/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; @@ -116,47 +115,6 @@ slots: { QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); } - -// this is only valid on linux -// FIXME: implement on windows, OSX, then test. -#if defined(Q_OS_LINUX) - void test_createShortcut_data() - { - QTest::addColumn<QString>("location"); - QTest::addColumn<QString>("dest"); - QTest::addColumn<QStringList>("args"); - QTest::addColumn<QString>("name"); - QTest::addColumn<QString>("iconLocation"); - QTest::addColumn<QByteArray>("result"); - - QTest::newRow("unix") << QDir::currentPath() - << "asdfDest" - << (QStringList() << "arg1" << "arg2") - << "asdf" - << QString() - #if defined(Q_OS_LINUX) - << GET_TEST_FILE("data/FileSystem-test_createShortcut-unix") - #elif defined(Q_OS_WIN) - << QByteArray() - #endif - ; - } - - void test_createShortcut() - { - QFETCH(QString, location); - QFETCH(QString, dest); - QFETCH(QStringList, args); - QFETCH(QString, name); - QFETCH(QString, iconLocation); - QFETCH(QByteArray, result); - - QVERIFY(FS::createShortCut(location, dest, args, name, iconLocation)); - QCOMPARE(QString::fromLocal8Bit(TestsInternal::readFile(location + QDir::separator() + name + ".desktop")), QString::fromLocal8Bit(result)); - - //QDir().remove(location); - } -#endif }; QTEST_GUILESS_MAIN(FileSystemTest) diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index 0368c32d..067104cf 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "GZip.h" #include <zlib.h> #include <QByteArray> @@ -67,7 +102,7 @@ bool GZip::zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes) return true; } - unsigned compLength = std::min(uncompressedBytes.size(), 16); + unsigned compLength = qMin(uncompressedBytes.size(), 16); compressedBytes.clear(); compressedBytes.resize(compLength); @@ -112,4 +147,4 @@ bool GZip::zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes) return false; } return true; -}
\ No newline at end of file +} diff --git a/launcher/GZip_test.cpp b/launcher/GZip_test.cpp index 3f4d181c..73859fbc 100644 --- a/launcher/GZip_test.cpp +++ b/launcher/GZip_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "GZip.h" #include <random> diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 8f68b95f..14e1cd47 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -41,6 +41,7 @@ #include "FileSystem.h" #include "MMCZip.h" #include "NullInstance.h" +#include "icons/IconList.h" #include "icons/IconUtils.h" #include "settings/INISettingsObject.h" @@ -59,9 +60,9 @@ #include "net/ChecksumValidator.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ScrollMessageBox.h" #include <algorithm> -#include <iterator> InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) { @@ -71,7 +72,8 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) bool InstanceImportTask::abort() { - m_filesNetJob->abort(); + if (m_filesNetJob) + m_filesNetJob->abort(); m_extractFuture.cancel(); return false; @@ -134,18 +136,20 @@ void InstanceImportTask::processZipPack() return; } - QStringList blacklist = {"instance.cfg", "manifest.json"}; - QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); - bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); - QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); - QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); + QuaZipDir packZipDir(m_packZip.get()); + + // https://docs.modrinth.com/docs/modpacks/format_definition/#storage + bool modrinthFound = packZipDir.exists("/modrinth.index.json"); + bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json"); QString root; - if(!mmcFound.isNull()) + + // NOTE: Prioritize modpack platforms that aren't searched for recursively. + // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example + if(modrinthFound) { - // process as MultiMC instance/pack - qDebug() << "MultiMC:" << mmcFound; - root = mmcFound; - m_modpackType = ModpackType::MultiMC; + // process as Modrinth pack + qDebug() << "Modrinth:" << modrinthFound; + m_modpackType = ModpackType::Modrinth; } else if (technicFound) { @@ -155,19 +159,25 @@ void InstanceImportTask::processZipPack() extractDir.cd(".minecraft"); m_modpackType = ModpackType::Technic; } - else if(!flameFound.isNull()) - { - // process as Flame pack - qDebug() << "Flame:" << flameFound; - root = flameFound; - m_modpackType = ModpackType::Flame; - } - else if(!modrinthFound.isNull()) + else { - // process as Modrinth pack - qDebug() << "Modrinth:" << modrinthFound; - root = modrinthFound; - m_modpackType = ModpackType::Modrinth; + QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); + QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); + + if (!mmcRoot.isNull()) + { + // process as MultiMC instance/pack + qDebug() << "MultiMC:" << mmcRoot; + root = mmcRoot; + m_modpackType = ModpackType::MultiMC; + } + else if(!flameRoot.isNull()) + { + // process as Flame pack + qDebug() << "Flame:" << flameRoot; + root = flameRoot; + m_modpackType = ModpackType::Flame; + } } if(m_modpackType == ModpackType::Unknown) { @@ -315,7 +325,7 @@ void InstanceImportTask::processFlame() // Hack to correct some 'special sauce'... if(mcVersion.endsWith('.')) { - mcVersion.remove(QRegExp("[.]+$")); + mcVersion.remove(QRegularExpression("[.]+$")); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } auto components = instance.getPackProfile(); @@ -384,61 +394,135 @@ void InstanceImportTask::processFlame() connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]() { auto results = m_modIdResolver->getResults(); - m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for(auto result: results.files) - { - QString filename = result.fileName; - if(!result.required) - { - filename += ".disabled"; + //first check for blocked mods + QString text; + auto anyBlocked = false; + for(const auto& result: results.files.values()) { + if (!result.resolved || result.url.isEmpty()) { + text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl); + anyBlocked = true; } + } + if(anyBlocked) { + qWarning() << "Blocked mods found, displaying mod list"; + + auto message_dialog = new ScrollMessageBox(m_parent, + tr("Blocked mods found"), + tr("The following mods were blocked on third party launchers.<br/>" + "You will need to manually download them and add them to the modpack"), + text); + message_dialog->setModal(true); + + if (message_dialog->exec()) { + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (const auto &result: m_modIdResolver->getResults().files) { + QString filename = result.fileName; + if (!result.required) { + filename += ".disabled"; + } - auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); - auto path = FS::PathCombine(m_stagingPath , relpath); + auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + auto path = FS::PathCombine(m_stagingPath, relpath); - switch(result.type) - { - case Flame::File::Type::Folder: - { - logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fall-through intentional, we treat these as plain old mods and dump them wherever. + switch (result.type) { + case Flame::File::Type::Folder: { + logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); + // fall-through intentional, we treat these as plain old mods and dump them wherever. + } + case Flame::File::Type::SingleFile: + case Flame::File::Type::Mod: { + if (!result.url.isEmpty()) { + qDebug() << "Will download" << result.url << "to" << path; + auto dl = Net::Download::makeFile(result.url, path); + m_filesNetJob->addNetAction(dl); + } + break; + } + case Flame::File::Type::Modpack: + logWarning( + tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg( + relpath)); + break; + case Flame::File::Type::Cmod2: + case Flame::File::Type::Ctoc: + case Flame::File::Type::Unknown: + logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); + break; + } + } + m_modIdResolver.reset(); + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); + } else { + m_modIdResolver.reset(); + emitFailed("Canceled"); + } + } else { + //TODO extract to function ? + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (const auto &result: m_modIdResolver->getResults().files) { + QString filename = result.fileName; + if (!result.required) { + filename += ".disabled"; } - case Flame::File::Type::SingleFile: - case Flame::File::Type::Mod: - { - qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::Download::makeFile(result.url, path); - m_filesNetJob->addNetAction(dl); - break; + + auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + auto path = FS::PathCombine(m_stagingPath, relpath); + + switch (result.type) { + case Flame::File::Type::Folder: { + logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); + // fall-through intentional, we treat these as plain old mods and dump them wherever. + } + case Flame::File::Type::SingleFile: + case Flame::File::Type::Mod: { + if (!result.url.isEmpty()) { + qDebug() << "Will download" << result.url << "to" << path; + auto dl = Net::Download::makeFile(result.url, path); + m_filesNetJob->addNetAction(dl); + } + break; + } + case Flame::File::Type::Modpack: + logWarning( + tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg( + relpath)); + break; + case Flame::File::Type::Cmod2: + case Flame::File::Type::Ctoc: + case Flame::File::Type::Unknown: + logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); + break; } - case Flame::File::Type::Modpack: - logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath)); - break; - case Flame::File::Type::Cmod2: - case Flame::File::Type::Ctoc: - case Flame::File::Type::Unknown: - logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); - break; } + m_modIdResolver.reset(); + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); } - m_modIdResolver.reset(); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() - { - m_filesNetJob.reset(); - emitSucceeded(); - } - ); - connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) - { - m_filesNetJob.reset(); - emitFailed(reason); - }); - connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) - { - setProgress(current, total); - }); - setStatus(tr("Downloading mods...")); - m_filesNetJob->start(); } ); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) @@ -497,6 +581,7 @@ void InstanceImportTask::processMultiMC() emitSucceeded(); } +// https://docs.modrinth.com/docs/modpacks/format_definition/ void InstanceImportTask::processModrinth() { std::vector<Modrinth::File> files; @@ -514,29 +599,33 @@ void InstanceImportTask::processModrinth() auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); bool had_optional = false; - for (auto& obj : jsonFiles) { + for (auto modInfo : jsonFiles) { Modrinth::File file; - file.path = Json::requireString(obj, "path"); - - auto env = Json::ensureObject(obj, "env"); - QString support = Json::ensureString(env, "client", "unsupported"); - if (support == "unsupported") { - continue; - } else if (support == "optional") { - // TODO: Make a review dialog for choosing which ones the user wants! - if (!had_optional) { - had_optional = true; - auto info = CustomMessageBox::selectable( - m_parent, tr("Optional mod detected!"), - tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), QMessageBox::Information); - info->exec(); - } + file.path = Json::requireString(modInfo, "path"); + + auto env = Json::ensureObject(modInfo, "env"); + // 'env' field is optional + if (!env.isEmpty()) { + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") { + continue; + } else if (support == "optional") { + // TODO: Make a review dialog for choosing which ones the user wants! + if (!had_optional) { + had_optional = true; + auto info = CustomMessageBox::selectable( + m_parent, tr("Optional mod detected!"), + tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), + QMessageBox::Information); + info->exec(); + } - if (file.path.endsWith(".jar")) - file.path += ".disabled"; + if (file.path.endsWith(".jar")) + file.path += ".disabled"; + } } - QJsonObject hashes = Json::requireObject(obj, "hashes"); + QJsonObject hashes = Json::requireObject(modInfo, "hashes"); QString hash; QCryptographicHash::Algorithm hashAlgorithm; hash = Json::ensureString(hashes, "sha1"); @@ -554,12 +643,28 @@ void InstanceImportTask::processModrinth() } file.hash = QByteArray::fromHex(hash.toLatin1()); file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) - file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) { - throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + + auto download_arr = Json::ensureArray(modInfo, "downloads"); + for(auto download : download_arr) { + qWarning() << download.toString(); + bool is_last = download.toString() == download_arr.last().toString(); + + auto download_url = QUrl(download.toString()); + + if (!download_url.isValid()) { + qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL") + .arg(download_url.toString(), file.path); + if(is_last && file.downloads.isEmpty()) + throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + } + else { + file.downloads.push_back(download_url); + } } + files.push_back(file); } @@ -590,16 +695,26 @@ void InstanceImportTask::processModrinth() emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } + + auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); - if (QFile::exists(overridePath)) { - QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - if (!QFile::rename(overridePath, mcPath)) { + auto override_path = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(override_path)) { + if (!QFile::rename(override_path, mcPath)) { emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); return; } } + // Do client overrides + auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(client_override_path)) { + if (!FS::overrideFolder(mcPath, client_override_path)) { + emitFailed(tr("Could not rename the client overrides folder:\n") + "client overrides"); + return; + } + } + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared<INISettingsObject>(configPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); @@ -607,11 +722,11 @@ void InstanceImportTask::processModrinth() components->buildingFromScratch(); components->setComponentVersion("net.minecraft", minecraftVersion, true); if (!fabricVersion.isEmpty()) - components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); if (!quiltVersion.isEmpty()) - components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true); + components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion); if (!forgeVersion.isEmpty()) - components->setComponentVersion("net.minecraftforge", forgeVersion, true); + components->setComponentVersion("net.minecraftforge", forgeVersion); if (m_instIcon != "default") { instance.setIconKey(m_instIcon); @@ -624,13 +739,24 @@ void InstanceImportTask::processModrinth() instance.saveNow(); m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for (auto &file : files) + for (auto file : files) { auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); - qDebug() << "Will download" << file.download << "to" << path; - auto dl = Net::Download::makeFile(file.download, path); + qDebug() << "Will try to download" << file.downloads.front() << "to" << path; + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_filesNetJob->addNetAction(dl); + + if (file.downloads.size() > 0) { + // FIXME: This really needs to be put into a ConcurrentTask of + // MultipleOptionsTask's , once those exist :) + connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{ + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + dl->succeeded(); + }); + } } connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 5e4d3235..b67d48f3 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -42,6 +42,7 @@ #include <QFutureWatcher> #include "settings/SettingsObject.h" #include "QObjectPtr.h" +#include "modplatform/flame/PackManifest.h" #include <nonstd/optional> @@ -59,6 +60,10 @@ public: bool canAbort() const override { return true; } bool abort() override; + const QVector<Flame::File> &getBlockedFiles() const + { + return m_blockedMods; + } protected: //! Entry point for tasks. @@ -87,6 +92,7 @@ private: /* data */ std::unique_ptr<QuaZip> m_packZip; QFuture<nonstd::optional<QStringList>> m_extractFuture; QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QVector<Flame::File> m_blockedMods; enum class ModpackType{ Unknown, MultiMC, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 847d897e..fb7103dd 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QDir> @@ -136,7 +156,7 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const { case InstancePointerRole: { - QVariant v = qVariantFromValue((void *)pdata); + QVariant v = QVariant::fromValue((void *)pdata); return v; } case InstanceIDRole: @@ -252,7 +272,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) QStringList InstanceList::getGroups() { - return m_groupNameCache.toList(); + return m_groupNameCache.values(); } void InstanceList::deleteGroup(const QString& name) @@ -353,7 +373,11 @@ QList< InstanceId > InstanceList::discoverInstances() out.append(id); qDebug() << "Found instance ID" << id; } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + instanceSet = QSet<QString>(out.begin(), out.end()); +#else instanceSet = out.toSet(); +#endif m_instancesProbed = true; return out; } @@ -547,8 +571,20 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) auto instanceRoot = FS::PathCombine(m_instDir, id); auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; - // TODO: Handle incompatible instances - inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + + instanceSettings->registerSetting("InstanceType", ""); + + QString inst_type = instanceSettings->get("InstanceType").toString(); + + // NOTE: Some PolyMC versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a OneSix instance + if (inst_type == "OneSix" || inst_type.isEmpty()) + { + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + } qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); return inst; } diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 357157d0..78fb7016 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -33,10 +33,10 @@ public: values.append(new LogPage(inst)); std::shared_ptr<MinecraftInstance> onesix = std::dynamic_pointer_cast<MinecraftInstance>(inst); values.append(new VersionPage(onesix.get())); - auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Mods"), "Loader-mods"); + auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); values.append(modsPage); - values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList(), "coremods", "coremods", tr("Core mods"), "Core-mods")); + values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); values.append(new ResourcePackPage(onesix.get())); values.append(new TexturePackPage(onesix.get())); values.append(new ShaderPackPage(onesix.get())); diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 17278d86..4ba319b8 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -1,10 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "JavaCommon.h" +#include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" #include <MMCStrings.h> +#include <QRegularExpression> bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) { - if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegExp("-Xm[sx]")) + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( @@ -18,7 +55,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) return false; } // block lunacy with passing required version to the JVM - if (jvmargs.contains(QRegExp("-version:.*"))) { + if (jvmargs.contains(QRegularExpression("-version:.*"))) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be allowed.\n" "This message will be displayed until you remove this from the JVM arguments."); @@ -65,6 +102,13 @@ void JavaCommon::javaBinaryWasBad(QWidget *parent, JavaCheckResult result) CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } +void JavaCommon::javaCheckNotFound(QWidget *parent) +{ + QString text; + text += QObject::tr("Java checker library could not be found. Please check your installation"); + CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); +} + void JavaCommon::TestCheck::run() { if (!JavaCommon::checkJVMArgs(m_args, m_parent)) @@ -72,6 +116,11 @@ void JavaCommon::TestCheck::run() emit finished(); return; } + if (JavaUtils::getJavaCheckPath().isEmpty()) { + javaCheckNotFound(m_parent); + emit finished(); + return; + } checker.reset(new JavaChecker()); connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, SLOT(checkFinished(JavaCheckResult))); diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index ca98145c..59cb7a67 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -10,12 +10,14 @@ namespace JavaCommon { bool checkJVMArgs(QString args, QWidget *parent); - // Show a dialog saying that the Java binary was not usable - void javaBinaryWasBad(QWidget *parent, JavaCheckResult result); - // Show a dialog saying that the Java binary was not usable because of bad options - void javaArgsWereBad(QWidget *parent, JavaCheckResult result); // Show a dialog saying that the Java binary was usable void javaWasOk(QWidget *parent, JavaCheckResult result); + // Show a dialog saying that the Java binary was not usable because of bad options + void javaArgsWereBad(QWidget *parent, JavaCheckResult result); + // Show a dialog saying that the Java binary was not usable + void javaBinaryWasBad(QWidget *parent, JavaCheckResult result); + // Show a dialog if we couldn't find Java Checker + void javaCheckNotFound(QWidget *parent); class TestCheck : public QObject { diff --git a/launcher/Json.cpp b/launcher/Json.cpp index 37ada1aa..06b3d3bd 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -1,4 +1,37 @@ -// Licensed under the Apache-2.0 license. See README.md for details. +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "Json.h" @@ -22,14 +55,6 @@ void write(const QJsonArray &array, const QString &filename) write(QJsonDocument(array), filename); } -QByteArray toBinary(const QJsonObject &obj) -{ - return QJsonDocument(obj).toBinaryData(); -} -QByteArray toBinary(const QJsonArray &array) -{ - return QJsonDocument(array).toBinaryData(); -} QByteArray toText(const QJsonObject &obj) { return QJsonDocument(obj).toJson(QJsonDocument::Compact); @@ -48,12 +73,8 @@ QJsonDocument requireDocument(const QByteArray &data, const QString &what) { if (isBinaryJson(data)) { - QJsonDocument doc = QJsonDocument::fromBinaryData(data); - if (doc.isNull()) - { - throw JsonException(what + ": Invalid JSON (binary JSON detected)"); - } - return doc; + // FIXME: Is this needed? + throw JsonException(what + ": Invalid JSON. Binary JSON unsupported"); } else { diff --git a/launcher/Json.h b/launcher/Json.h index f2e68f0c..b11a356c 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -1,4 +1,37 @@ -// Licensed under the Apache-2.0 license. See README.md for details. +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #pragma once @@ -29,8 +62,6 @@ void write(const QJsonObject &object, const QString &filename); /// @throw FileSystemException void write(const QJsonArray &array, const QString &filename); -QByteArray toBinary(const QJsonObject &obj); -QByteArray toBinary(const QJsonArray &array); QByteArray toText(const QJsonObject &obj); QByteArray toText(const QJsonArray &array); diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 002c08b9..d36ee3fe 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -105,6 +105,11 @@ void LaunchController::decideAccount() // Open the account manager. APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); } + else if (reply == QMessageBox::No) + { + // Do not open "profile select" dialog. + return; + } } m_accountToUse = accounts->defaultAccount(); diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index 2479f4ff..fbdeed8f 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "LoggedProcess.h" #include "MessageLevel.h" #include <QDebug> @@ -8,7 +43,11 @@ LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent) connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus))); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(this, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError))); +#else connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError))); +#endif connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); } @@ -157,19 +196,6 @@ void LoggedProcess::on_stateChange(QProcess::ProcessState state) } } -#if defined Q_OS_WIN32 -#include <windows.h> -#endif - -qint64 LoggedProcess::processId() const -{ -#ifdef Q_OS_WIN - return pid() ? pid()->dwProcessId : 0; -#else - return pid(); -#endif -} - void LoggedProcess::setDetachable(bool detachable) { m_is_detachable = detachable; diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h index e52b8a7b..61e74bd9 100644 --- a/launcher/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * 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 @@ -43,7 +63,6 @@ public: State state() const; int exitCode() const; - qint64 processId() const; void setDetachable(bool detachable); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 8591fcc0..f20d6dff 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -151,23 +151,23 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const continue; if (mod.type() == Mod::MOD_ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles)) + if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles)) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } } else if (mod.type() == Mod::MOD_SINGLEFILE) { // FIXME: buggy - does not work with addedFiles - auto filename = mod.filename(); + auto filename = mod.fileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); @@ -176,7 +176,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles - auto filename = mod.filename(); + auto filename = mod.fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); @@ -193,7 +193,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder " << filename.fileName() << " from " @@ -204,7 +204,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add unknown mod type" << mod.fileinfo().fileName() << "to the jar."; return false; } } @@ -305,7 +305,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & QString path; if(name.contains('/') && !name.endsWith('/')){ path = name.section('/', 0, -2) + "/"; - FS::ensureFolderPathExists(path); + FS::ensureFolderPathExists(FS::PathCombine(target, path)); name = name.split('/').last(); } @@ -421,7 +421,7 @@ bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& s continue; } - files->append(e.filePath()); // we want the original paths for MMCZip::compressDirFiles + files->append(e); // we want the original paths for MMCZip::compressDirFiles } return true; } diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 08a02d29..41856fb5 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -1,24 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + #include "ModDownloadTask.h" + #include "Application.h" +#include "minecraft/mod/ModFolderModel.h" -ModDownloadTask::ModDownloadTask(const QUrl sourceUrl,const QString filename, const std::shared_ptr<ModFolderModel> mods) -: m_sourceUrl(sourceUrl), mods(mods), filename(filename) { -} +ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed) + : m_mod(mod), m_mod_version(version), mods(mods) +{ + if (is_indexed) { + m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); -void ModDownloadTask::executeTask() { - setStatus(tr("Downloading mod:\n%1").arg(m_sourceUrl.toString())); + addTask(m_update_task); + } m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::Download::makeFile(m_sourceUrl, mods->dir().absoluteFilePath(filename))); + m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl)); + + m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename()))); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed); - m_filesNetJob->start(); + + addTask(m_filesNetJob); + } void ModDownloadTask::downloadSucceeded() { - emitSucceeded(); m_filesNetJob.reset(); } @@ -32,8 +58,3 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } - -bool ModDownloadTask::abort() { - return m_filesNetJob->abort(); -} - diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index ddada5a2..b3c25909 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -1,28 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + #pragma once -#include "QObjectPtr.h" -#include "tasks/Task.h" -#include "minecraft/mod/ModFolderModel.h" + #include "net/NetJob.h" -#include <QUrl> +#include "tasks/SequentialTask.h" +#include "modplatform/ModIndex.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" -class ModDownloadTask : public Task { +class ModFolderModel; + +class ModDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr<ModFolderModel> mods); - const QString& getFilename() const { return filename; } - -public slots: - bool abort() override; -protected: - //! Entry point for tasks. - void executeTask() override; + explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed); + const QString& getFilename() const { return m_mod_version.fileName; } private: - QUrl m_sourceUrl; - NetJob::Ptr m_filesNetJob; + ModPlatform::IndexedPack m_mod; + ModPlatform::IndexedVersion m_mod_version; const std::shared_ptr<ModFolderModel> mods; - const QString filename; + + NetJob::Ptr m_filesNetJob; + LocalModUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index ed421433..9b0a9331 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -39,6 +39,10 @@ public: { return QProcessEnvironment(); } + QProcessEnvironment createLaunchEnvironment() override + { + return QProcessEnvironment(); + } QMap<QString, QString> getVariables() const override { return QMap<QString, QString>(); diff --git a/launcher/Version.h b/launcher/Version.h index 9fe12d6d..aceb7a07 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -1,6 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> +#include <QStringView> #include <QList> class QUrl; @@ -39,13 +75,21 @@ private: break; } } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto numPart = QStringView{m_fullString}.left(cutoff); +#else auto numPart = m_fullString.leftRef(cutoff); +#endif if(numPart.size()) { numValid = true; m_numPart = numPart.toInt(); } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto stringPart = QStringView{m_fullString}.mid(cutoff); +#else auto stringPart = m_fullString.midRef(cutoff); +#endif if(stringPart.size()) { m_stringPart = stringPart.toString(); diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index b9a87c9c..032f21f9 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "VersionProxyModel.h" #include "Application.h" #include <QSortFilterProxyModel> @@ -208,7 +243,8 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const { return APPLICATION->getThemedIcon("bug"); } - auto pixmap = QPixmapCache::find("placeholder"); + QPixmap pixmap; + QPixmapCache::find("placeholder", &pixmap); if(!pixmap) { QPixmap px(16,16); @@ -216,7 +252,7 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const QPixmapCache::insert("placeholder", px); return px; } - return *pixmap; + return pixmap; } } default: diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index c269d10a..3a223d1b 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "IconList.h" @@ -56,6 +76,15 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren emit iconUpdated({}); } +void IconList::sortIconList() +{ + qDebug() << "Sorting icon list..."; + std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { + return a.m_key.localeAwareCompare(b.m_key) < 0; + }); + reindex(); +} + void IconList::directoryChanged(const QString &path) { QDir new_dir (path); @@ -77,7 +106,11 @@ void IconList::directoryChanged(const QString &path) QString &foo = (*it); foo = m_dir.filePath(foo); } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QSet<QString> new_set(new_list.begin(), new_list.end()); +#else auto new_set = new_list.toSet(); +#endif QList<QString> current_list; for (auto &it : icons) { @@ -85,7 +118,11 @@ void IconList::directoryChanged(const QString &path) continue; current_list.push_back(it.m_images[IconType::FileBased].filename); } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QSet<QString> current_set(current_list.begin(), current_list.end()); +#else QSet<QString> current_set = current_list.toSet(); +#endif QSet<QString> to_remove = current_set; to_remove -= new_set; @@ -141,6 +178,8 @@ void IconList::directoryChanged(const QString &path) emit iconUpdated(key); } } + + sortIconList(); } void IconList::fileChanged(const QString &path) @@ -273,7 +312,7 @@ void IconList::installIcons(const QStringList &iconFiles) QFileInfo fileinfo(file); if (!fileinfo.isReadable() || !fileinfo.isFile()) continue; - QString target = FS::PathCombine(m_dir.dirName(), fileinfo.fileName()); + QString target = FS::PathCombine(getDirectory(), fileinfo.fileName()); QString suffix = fileinfo.suffix(); if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") @@ -290,7 +329,7 @@ void IconList::installIcon(const QString &file, const QString &name) if(!fileinfo.isReadable() || !fileinfo.isFile()) return; - QString target = FS::PathCombine(m_dir.dirName(), name); + QString target = FS::PathCombine(getDirectory(), name); QFile::copy(file, target); } diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index ebbb52e2..f9f49e7f 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -71,6 +71,7 @@ private: // hide assign op IconList &operator=(const IconList &) = delete; void reindex(); + void sortIconList(); public slots: void directoryChanged(const QString &path); diff --git a/launcher/icons/MMCIcon.cpp b/launcher/icons/MMCIcon.cpp index f0b82ec9..436ef75f 100644 --- a/launcher/icons/MMCIcon.cpp +++ b/launcher/icons/MMCIcon.cpp @@ -1,21 +1,41 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "MMCIcon.h" #include <QFileInfo> -#include <xdgicon.h> +#include <QIcon> IconType operator--(IconType &t, int) { @@ -63,7 +83,7 @@ QIcon MMCIcon::icon() const if(!icon.isNull()) return icon; // FIXME: inject this. - return XdgIcon::fromTheme(m_images[m_current_type].key); + return QIcon::fromTheme(m_images[m_current_type].key); } void MMCIcon::remove(IconType rm_type) diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 946599c5..041583d1 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "JavaChecker.h" #include <QFile> @@ -16,7 +51,13 @@ JavaChecker::JavaChecker(QObject *parent) : QObject(parent) void JavaChecker::performCheck() { - QString checkerJar = FS::PathCombine(APPLICATION->getJarsPath(), "JavaCheck.jar"); + QString checkerJar = JavaUtils::getJavaCheckPath(); + + if (checkerJar.isEmpty()) + { + qDebug() << "Java checker library could not be found. Please check your installation."; + return; + } QStringList args; @@ -47,7 +88,11 @@ void JavaChecker::performCheck() qDebug() << "Running java checker: " + m_path + args.join(" ");; connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus))); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(process.get(), SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); +#else connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); +#endif connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady())); connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady())); connect(&killTimer, SIGNAL(timeout()), SLOT(timeout())); @@ -99,7 +144,12 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) bool success = true; QMap<QString, QString> results; + +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); +#else QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); +#endif for(QString line : lines) { line = line.trimmed(); @@ -108,7 +158,11 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) continue; } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto parts = line.split('=', Qt::SkipEmptyParts); +#else auto parts = line.split('=', QString::SkipEmptyParts); +#endif if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 9b745095..0249bd22 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -1,21 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QtNetwork> #include <QtXml> -#include <QRegExp> #include <QDebug> @@ -81,7 +100,7 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const switch (role) { case VersionPointerRole: - return qVariantFromValue(m_vlist[index.row()]); + return QVariant::fromValue(m_vlist[index.row()]); case VersionIdRole: return version->descriptor(); case VersionRole: diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 65a8b1db..c2b776ae 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QStringList> @@ -24,6 +44,7 @@ #include "java/JavaUtils.h" #include "java/JavaInstallList.h" #include "FileSystem.h" +#include "Application.h" #define IBUS "@im=ibus" @@ -176,25 +197,25 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString archType = "32"; HKEY jreKey; - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, keyName.toStdString().c_str(), 0, + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == ERROR_SUCCESS) { // Read the current type version from the registry. // This will be used to find any key that contains the JavaHome value. char *value = new char[0]; DWORD valueSz = 0; - if (RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz) == + if (RegQueryValueExW(jreKey, L"CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz) == ERROR_MORE_DATA) { value = new char[valueSz]; - RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); + RegQueryValueExW(jreKey, L"CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); } TCHAR subKeyName[255]; DWORD subKeyNameSize, numSubKeys, retCode; // Get the number of subkeys - RegQueryInfoKey(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, + RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); // Iterate until RegEnumKeyEx fails @@ -203,31 +224,32 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString for (DWORD i = 0; i < numSubKeys; i++) { subKeyNameSize = 255; - retCode = RegEnumKeyEx(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, - NULL); + retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, + NULL); + QString newSubkeyName = QString::fromWCharArray(subKeyName); if (retCode == ERROR_SUCCESS) { // Now open the registry key for the version that we just got. - QString newKeyName = keyName + "\\" + subKeyName + subkeySuffix; + QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; HKEY newKey; - if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0, - KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS) + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, + KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS) { // Read the JavaHome value to find where Java is installed. value = new char[0]; valueSz = 0; - if (RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, - &valueSz) == ERROR_MORE_DATA) + if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value, + &valueSz) == ERROR_MORE_DATA) { value = new char[valueSz]; - RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, - &valueSz); + RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value, + &valueSz); // Now, we construct the version object and add it to the list. JavaInstallPtr javaVersion(new JavaInstall()); - javaVersion->id = subKeyName; + javaVersion->id = newSubkeyName; javaVersion->arch = archType; javaVersion->path = QDir(FS::PathCombine(value, "bin")).absoluteFilePath("javaw.exe"); @@ -437,3 +459,8 @@ QList<QString> JavaUtils::FindJavaPaths() return addJavasFromEnv(javas); } #endif + +QString JavaUtils::getJavaCheckPath() +{ + return APPLICATION->getJarPath("JavaCheck.jar"); +} diff --git a/launcher/java/JavaUtils.h b/launcher/java/JavaUtils.h index 3152d143..26d8003b 100644 --- a/launcher/java/JavaUtils.h +++ b/launcher/java/JavaUtils.h @@ -39,4 +39,6 @@ public: #ifdef Q_OS_WIN QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); #endif + + static QString getJavaCheckPath(); }; diff --git a/launcher/java/JavaVersion_test.cpp b/launcher/java/JavaVersion_test.cpp index 10ae13a7..545947ef 100644 --- a/launcher/java/JavaVersion_test.cpp +++ b/launcher/java/JavaVersion_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "java/JavaVersion.h" diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index d5442a2b..3aa95052 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -282,6 +282,23 @@ void LaunchTask::emitFailed(QString reason) Task::emitFailed(reason); } +void LaunchTask::substituteVariables(const QStringList &args) const +{ + auto variables = m_instance->getVariables(); + auto envVariables = QProcessEnvironment::systemEnvironment(); + + for (auto arg : args) { + for (auto key : variables) + { + arg.replace("$" + key, variables.value(key)); + } + for (auto env : envVariables.keys()) + { + arg.replace("$" + env, envVariables.value(env)); + } + } +} + QString LaunchTask::substituteVariables(const QString &cmd) const { QString out = cmd; diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index a1e891ac..2efe1fa2 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -1,18 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Authors: Orochimarufan <orochimarufan.x3@gmail.com> + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * http://www.apache.org/licenses/LICENSE-2.0 + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. * - * 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. + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan <orochimarufan.x3@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -85,6 +105,7 @@ public: /* methods */ shared_qobject_ptr<LogModel> getLogModel(); public: + void substituteVariables(const QStringList &args) const; QString substituteVariables(const QString &cmd) const; QString censorPrivateInfo(QString in); diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 3226fae7..db56b652 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -34,6 +34,7 @@ */ #include "CheckJava.h" +#include "java/JavaUtils.h" #include <launch/LaunchTask.h> #include <FileSystem.h> #include <QStandardPaths> @@ -71,15 +72,26 @@ void CheckJava::executeTask() emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::Launcher); } + if (JavaUtils::getJavaCheckPath().isEmpty()) + { + const char *reason = QT_TR_NOOP("Java checker library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + QFileInfo javaInfo(realJavaPath); qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); + auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); auto storedVendor = settings->get("JavaVendor").toString(); m_javaUnixTime = javaUnixTime; // if timestamps are not the same, or something is missing, check! - if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedVendor.size() == 0) + if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 + || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 + || storedVendor.size() == 0) { m_JavaChecker = new JavaChecker(); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); @@ -92,8 +104,9 @@ void CheckJava::executeTask() { auto verString = instance->settings()->get("JavaVersion").toString(); auto archString = instance->settings()->get("JavaArchitecture").toString(); + auto realArchString = settings->get("JavaRealArchitecture").toString(); auto vendorString = instance->settings()->get("JavaVendor").toString(); - printJavaInfo(verString, archString, vendorString); + printJavaInfo(verString, archString, realArchString, vendorString); } emitSucceeded(); } @@ -124,10 +137,11 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) case JavaCheckResult::Validity::Valid: { auto instance = m_parent->instance(); - printJavaInfo(result.javaVersion.toString(), result.realPlatform, result.javaVendor); + printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor); printSystemInfo(true, result.is_64bit); instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaArchitecture", result.mojangPlatform); + instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaVendor", result.javaVendor); instance->settings()->set("JavaTimestamp", m_javaUnixTime); emitSucceeded(); @@ -136,9 +150,10 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) } } -void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor) +void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString & vendor) { - emit logLine(QString("Java is version %1, using %2 architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::Launcher); + emit logLine(QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n") + .arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); } void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit) diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h index 68cd618b..d084b132 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -35,7 +35,7 @@ private slots: void checkJavaFinished(JavaCheckResult result); private: - void printJavaInfo(const QString & version, const QString & architecture, const QString & vendor); + void printJavaInfo(const QString & version, const QString & architecture, const QString & realArchitecture, const QString & vendor); void printSystemInfo(bool javaIsKnown, bool javaIs64bit); private: diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 143eb441..cf765bc0 100644 --- a/launcher/launch/steps/PostLaunchCommand.cpp +++ b/launcher/launch/steps/PostLaunchCommand.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "PostLaunchCommand.h" @@ -27,9 +47,19 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask *parent) : LaunchStep(parent) void PostLaunchCommand::executeTask() { + //FIXME: where to put this? +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + auto args = QProcess::splitCommand(m_command); + m_parent->substituteVariables(args); + + emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher); + const QString program = args.takeFirst(); + m_process.start(program, args); +#else QString postlaunch_cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Post-Launch command: %1").arg(postlaunch_cmd), MessageLevel::Launcher); m_process.start(postlaunch_cmd); +#endif } void PostLaunchCommand::on_state(LoggedProcess::State state) diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 1a0889c8..bf7d27eb 100644 --- a/launcher/launch/steps/PreLaunchCommand.cpp +++ b/launcher/launch/steps/PreLaunchCommand.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "PreLaunchCommand.h" @@ -28,9 +48,18 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask *parent) : LaunchStep(parent) void PreLaunchCommand::executeTask() { //FIXME: where to put this? +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + auto args = QProcess::splitCommand(m_command); + m_parent->substituteVariables(args); + + emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher); + const QString program = args.takeFirst(); + m_process.start(program, args); +#else QString prelaunch_cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd), MessageLevel::Launcher); m_process.start(prelaunch_cmd); +#endif } void PreLaunchCommand::on_state(LoggedProcess::State state) diff --git a/launcher/main.cpp b/launcher/main.cpp index 85c5fdee..85fe1260 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "Application.h" // #define BREAK_INFINITE_LOOP @@ -24,11 +59,9 @@ int main(int argc, char *argv[]) return 42; #endif +#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif // initialize Qt diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 84155922..de4e1012 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -75,7 +75,16 @@ Meta::BaseEntity::~BaseEntity() QUrl Meta::BaseEntity::url() const { - return QUrl(BuildConfig.META_URL).resolved(localFilename()); + auto s = APPLICATION->settings(); + QString metaOverride = s->get("MetaURLOverride").toString(); + if(metaOverride.isEmpty()) + { + return QUrl(BuildConfig.META_URL).resolved(localFilename()); + } + else + { + return QUrl(metaOverride).resolved(localFilename()); + } } bool Meta::BaseEntity::loadLocalFile() diff --git a/launcher/meta/Index_test.cpp b/launcher/meta/Index_test.cpp index 5d3ddc50..261858c4 100644 --- a/launcher/meta/Index_test.cpp +++ b/launcher/meta/Index_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "meta/Index.h" #include "meta/VersionList.h" diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 7290aeb4..15062c2b 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * 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 <QFileInfo> @@ -297,7 +317,7 @@ NetAction::Ptr AssetObject::getDownloadAction() auto rawHash = QByteArray::fromHex(hash.toLatin1()); objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); } - objectDL->m_total_progress = size; + objectDL->setProgress(objectDL->getProgress(), size); return objectDL; } return nullptr; diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index ff7ed0af..6db21622 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -197,6 +197,10 @@ void ComponentUpdateTask::loadComponents() { remoteLoadFailed(taskIndex, error); }); + connect(indexLoadTask.get(), &Task::aborted, [=]() + { + remoteLoadFailed(taskIndex, tr("Aborted")); + }); taskIndex++; } } @@ -243,6 +247,10 @@ void ComponentUpdateTask::loadComponents() { remoteLoadFailed(taskIndex, error); }); + connect(loadTask.get(), &Task::aborted, [=]() + { + remoteLoadFailed(taskIndex, tr("Aborted")); + }); RemoteLoadStatus status; status.type = loadType; status.PackProfileIndex = componentIndex; diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index d9bb0207..27514ab9 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -1,7 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> #include <QStringList> +#include <QRegularExpression> #include "DefaultVariable.h" struct GradleSpecifier @@ -25,20 +61,21 @@ struct GradleSpecifier 4 "jdk15" 5 "jar" */ - QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?"); - m_valid = matcher.exactMatch(value); + QRegularExpression matcher(QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); + QRegularExpressionMatch match = matcher.match(value); + m_valid = match.hasMatch(); if(!m_valid) { m_invalidValue = value; return *this; } - auto elements = matcher.capturedTexts(); - m_groupId = elements[1]; - m_artifactId = elements[2]; - m_version = elements[3]; - m_classifier = elements[4]; - if(!elements[5].isEmpty()) + auto elements = match.captured(); + m_groupId = match.captured(1); + m_artifactId = match.captured(2); + m_version = match.captured(3); + m_classifier = match.captured(4); + if(match.lastCapturedIndex() >= 5) { - m_extension = elements[5]; + m_extension = match.captured(5); } return *this; } diff --git a/launcher/minecraft/GradleSpecifier_test.cpp b/launcher/minecraft/GradleSpecifier_test.cpp index 0900c9d8..a062dfac 100644 --- a/launcher/minecraft/GradleSpecifier_test.cpp +++ b/launcher/minecraft/GradleSpecifier_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "minecraft/GradleSpecifier.h" diff --git a/launcher/minecraft/Library_test.cpp b/launcher/minecraft/Library_test.cpp index 47531ad6..834dd558 100644 --- a/launcher/minecraft/Library_test.cpp +++ b/launcher/minecraft/Library_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "minecraft/MojangVersionFormat.h" #include "minecraft/OneSixVersionFormat.h" @@ -11,15 +10,14 @@ class LibraryTest : public QObject { Q_OBJECT private: - LibraryPtr readMojangJson(const char *file) + LibraryPtr readMojangJson(const QString path) { - auto path = QFINDTESTDATA(file); QFile jsonFile(path); jsonFile.open(QIODevice::ReadOnly); auto data = jsonFile.readAll(); jsonFile.close(); ProblemContainer problems; - return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), file); + return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), path); } // get absolute path to expected storage, assuming default cache prefix QStringList getStorage(QString relative) @@ -32,7 +30,7 @@ slots: { cache.reset(new HttpMetaCache()); cache->addBase("libraries", QDir("libraries").absolutePath()); - dataDir = QDir("data").absolutePath(); + dataDir = QDir(QFINDTESTDATA("testdata")).absolutePath(); } void test_legacy() { @@ -74,14 +72,14 @@ slots: QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); - auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString("data")); + auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QFINDTESTDATA("testdata")); QCOMPARE(downloads.size(), 0); qDebug() << failedFiles; QCOMPARE(failedFiles.size(), 0); QStringList jar, native, native32, native64; - test.getApplicableFiles(currentSystem, jar, native, native32, native64, QString("data")); - QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()}); + test.getApplicableFiles(currentSystem, jar, native, native32, native64, QFINDTESTDATA("testdata")); + QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()}); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); @@ -167,20 +165,20 @@ slots: test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; - test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString("data")); + test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QFINDTESTDATA("testdata")); QCOMPARE(jar, {}); QCOMPARE(native, {}); - QCOMPARE(native32, {QFileInfo("data/testname-testversion-linux-32.jar").absoluteFilePath()}); - QCOMPARE(native64, {QFileInfo("data/testname-testversion-linux-64.jar").absoluteFilePath()}); + QCOMPARE(native32, {QFileInfo(QFINDTESTDATA("testdata/testname-testversion-linux-32.jar")).absoluteFilePath()}); + QCOMPARE(native64, {QFileInfo(QFINDTESTDATA("testdata") + "/testname-testversion-linux-64.jar").absoluteFilePath()}); QStringList failedFiles; - auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); + auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata")); QCOMPARE(dls.size(), 0); - QCOMPARE(failedFiles, {"data/testname-testversion-linux-64.jar"}); + QCOMPARE(failedFiles, {QFileInfo(QFINDTESTDATA("testdata") + "/testname-testversion-linux-64.jar").absoluteFilePath()}); } } void test_onenine() { - auto test = readMojangJson("data/lib-simple.json"); + auto test = readMojangJson(QFINDTESTDATA("testdata/lib-simple.json")); { QStringList jar, native, native32, native64; test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); @@ -199,41 +197,41 @@ slots: test->setHint("local"); { QStringList jar, native, native32, native64; - test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString("data")); - QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()}); + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata")); + QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()}); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } { QStringList failedFiles; - auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_local_override() { - auto test = readMojangJson("data/lib-simple.json"); + auto test = readMojangJson(QFINDTESTDATA("testdata/lib-simple.json")); test->setHint("local"); { QStringList jar, native, native32, native64; - test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString("data")); - QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()}); + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata")); + QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()}); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } { QStringList failedFiles; - auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_native() { - auto test = readMojangJson("data/lib-native.json"); + auto test = readMojangJson(QFINDTESTDATA("testdata/lib-native.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); QCOMPARE(jar, QStringList()); @@ -248,7 +246,7 @@ slots: } void test_onenine_native_arch() { - auto test = readMojangJson("data/lib-native-arch.json"); + auto test = readMojangJson(QFINDTESTDATA("testdata/lib-native-arch.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(Os_Windows, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index e20dc24c..abc022b6 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -153,6 +154,12 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + // Peformance related options + auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); + m_settings->registerOverride(globalSettings->getSetting("EnableFeralGamemode"), performanceOverride); + m_settings->registerOverride(globalSettings->getSetting("EnableMangoHud"), performanceOverride); + m_settings->registerOverride(globalSettings->getSetting("UseDiscreteGpu"), performanceOverride); + // Game time auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); @@ -167,6 +174,8 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + m_settings->set("InstanceType", "OneSix"); + m_components.reset(new PackProfile(this)); } @@ -432,27 +441,57 @@ QProcessEnvironment MinecraftInstance::createEnvironment() return env; } +QProcessEnvironment MinecraftInstance::createLaunchEnvironment() +{ + // prepare the process environment + QProcessEnvironment env = createEnvironment(); + +#ifdef Q_OS_LINUX + if (settings()->get("EnableMangoHud").toBool()) + { + auto preload = env.value("LD_PRELOAD", "") + ":libMangoHud_dlsym.so:libMangoHud.so"; + auto lib_path = env.value("LD_LIBRARY_PATH", "") + ":/usr/local/$LIB/mangohud/:/usr/$LIB/mangohud/"; + + env.insert("LD_PRELOAD", preload); + env.insert("LD_LIBRARY_PATH", lib_path); + env.insert("MANGOHUD", "1"); + } + + if (settings()->get("UseDiscreteGpu").toBool()) + { + // Open Source Drivers + env.insert("DRI_PRIME", "1"); + // Proprietary Nvidia Drivers + env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); + env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); + env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + } +#endif + + return env; +} + static QString replaceTokensIn(QString text, QMap<QString, QString> with) { + // TODO: does this still work?? QString result; - QRegExp token_regexp("\\$\\{(.+)\\}"); - token_regexp.setMinimal(true); + QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); QStringList list; - int tail = 0; - int head = 0; - while ((head = token_regexp.indexIn(text, head)) != -1) + QRegularExpressionMatchIterator i = token_regexp.globalMatch(text); + int lastCapturedEnd = 0; + while (i.hasNext()) { - result.append(text.mid(tail, head - tail)); - QString key = token_regexp.cap(1); + QRegularExpressionMatch match = i.next(); + result.append(text.mid(lastCapturedEnd, match.capturedStart())); + QString key = match.captured(1); auto iter = with.find(key); if (iter != with.end()) { result.append(*iter); } - head += token_regexp.matchedLength(); - tail = head; + lastCapturedEnd = match.capturedEnd(); } - result.append(text.mid(tail)); + result.append(text.mid(lastCapturedEnd)); return result; } @@ -487,9 +526,8 @@ QStringList MinecraftInstance::processMinecraftArgs( } } - // blatant self-promotion. - token_mapping["profile_name"] = token_mapping["version_name"] = BuildConfig.LAUNCHER_NAME; - + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); token_mapping["version_type"] = profile->getMinecraftVersionType(); QString absRootDir = QDir(gameRoot()).absolutePath(); @@ -502,7 +540,11 @@ QStringList MinecraftInstance::processMinecraftArgs( token_mapping["assets_root"] = absAssetsDir; token_mapping["assets_index_name"] = assets->id; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); +#else QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); +#endif for (int i = 0; i < parts.length(); i++) { parts[i] = replaceTokensIn(parts[i], token_mapping); @@ -659,23 +701,23 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << QString("%1:").arg(label); auto modList = model.allMods(); std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { - auto aName = a.filename().completeBaseName(); - auto bName = b.filename().completeBaseName(); + auto aName = a.fileinfo().completeBaseName(); + auto bName = b.fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); for(auto & mod: modList) { if(mod.type() == Mod::MOD_FOLDER) { - out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)"; + out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)"; continue; } if(mod.enabled()) { - out << u8" [✔️] " + mod.filename().completeBaseName(); + out << u8" [✔️] " + mod.fileinfo().completeBaseName(); } else { - out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)"; + out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)"; } } @@ -745,7 +787,9 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess { addToFilter(sessionRef.session, tr("<SESSION ID>")); } - addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>")); + if (sessionRef.access_token != "offline") { + addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>")); + } if(sessionRef.client_token.size()) { addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>")); } @@ -824,8 +868,16 @@ QString MinecraftInstance::getStatusbarDescription() traits.append(tr("broken")); } + QString mcVersion = m_components->getComponentVersion("net.minecraft"); + if (mcVersion.isEmpty()) + { + // Load component info if needed + m_components->reload(Net::Mode::Offline); + mcVersion = m_components->getComponentVersion("net.minecraft"); + } + QString description; - description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName())); + description.append(tr("Minecraft %1").arg(mcVersion)); if(m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0) { @@ -1013,7 +1065,8 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new ModFolderModel(modsRoot())); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed)); m_loader_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } @@ -1024,7 +1077,8 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new ModFolderModel(coreModsDir())); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed)); m_core_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index fda58aa7..05450d41 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -91,6 +91,7 @@ public: /// create an environment for launching processes QProcessEnvironment createEnvironment() override; + QProcessEnvironment createLaunchEnvironment() override; /// guess log level from a line of minecraft log MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) override; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index 79b0c484..d72bc7be 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -20,6 +20,7 @@ void MinecraftLoadAndCheck::executeTask() } connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::subtaskSucceeded); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); + connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); }); connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); } diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp index 32e9cbb6..0ce0c347 100644 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ b/launcher/minecraft/MinecraftUpdate.cpp @@ -98,6 +98,7 @@ void MinecraftUpdate::next() auto task = m_tasks[m_currentTask - 1]; disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + disconnect(task.get(), &Task::aborted, this, &Task::abort); disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); } @@ -115,6 +116,7 @@ void MinecraftUpdate::next() } connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + connect(task.get(), &Task::aborted, this, &Task::abort); connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); // if the task is already running, do not start it again diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h index 9ebef656..acf2eb86 100644 --- a/launcher/minecraft/MinecraftUpdate.h +++ b/launcher/minecraft/MinecraftUpdate.h @@ -27,6 +27,8 @@ class MinecraftVersion; class MinecraftInstance; +// FIXME: This looks very similar to a SequentialTask. Maybe we can reduce code duplications? :^) + class MinecraftUpdate : public Task { Q_OBJECT diff --git a/launcher/minecraft/MojangDownloadInfo.h b/launcher/minecraft/MojangDownloadInfo.h index 88f87287..13e27e15 100644 --- a/launcher/minecraft/MojangDownloadInfo.h +++ b/launcher/minecraft/MojangDownloadInfo.h @@ -65,7 +65,7 @@ struct MojangAssetIndexInfo : public MojangDownloadInfo // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ if(id == "legacy") { - url = "https://launchermeta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; + url = "https://piston-meta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; } // HACK else diff --git a/launcher/minecraft/MojangVersionFormat_test.cpp b/launcher/minecraft/MojangVersionFormat_test.cpp index 9d095340..71df784b 100644 --- a/launcher/minecraft/MojangVersionFormat_test.cpp +++ b/launcher/minecraft/MojangVersionFormat_test.cpp @@ -1,6 +1,5 @@ #include <QTest> #include <QDebug> -#include "TestUtil.h" #include "minecraft/MojangVersionFormat.h" @@ -8,9 +7,8 @@ class MojangVersionFormatTest : public QObject { Q_OBJECT - static QJsonDocument readJson(const char *file) + static QJsonDocument readJson(const QString path) { - auto path = QFINDTESTDATA(file); QFile jsonFile(path); jsonFile.open(QIODevice::ReadOnly); auto data = jsonFile.readAll(); @@ -31,7 +29,7 @@ private slots: void test_Through_Simple() { - QJsonDocument doc = readJson("data/1.9-simple.json"); + QJsonDocument doc = readJson(QFINDTESTDATA("testdata/1.9-simple.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-simple-passthorugh.json", doc2); @@ -41,7 +39,7 @@ slots: void test_Through() { - QJsonDocument doc = readJson("data/1.9.json"); + QJsonDocument doc = readJson(QFINDTESTDATA("testdata/1.9.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-passthorugh.json", doc2); diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 879f18c1..cec4a55b 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "OneSixVersionFormat.h" #include <Json.h> #include "minecraft/Agent.h" @@ -296,7 +331,7 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } writeString(root, "appletClass", patch->appletClass); writeStringList(root, "+tweakers", patch->addTweakers); - writeStringList(root, "+traits", patch->traits.toList()); + writeStringList(root, "+traits", patch->traits.values()); if (!patch->libraries.isEmpty()) { QJsonArray array; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index d53f41e1..5e76b892 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QFile> @@ -36,6 +56,13 @@ #include "ComponentUpdateTask.h" #include "Application.h" +#include "modplatform/ModAPI.h" + +static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{ + {"net.minecraftforge", ModAPI::Forge}, + {"net.fabricmc.fabric-loader", ModAPI::Fabric}, + {"org.quiltmc.quilt-loader", ModAPI::Quilt} +}; PackProfile::PackProfile(MinecraftInstance * instance) : QAbstractListModel() @@ -339,6 +366,7 @@ void PackProfile::resolve(Net::Mode netmode) d->m_updateTask.reset(updateTask); connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); + connect(updateTask, &ComponentUpdateTask::aborted, this, [this]{ updateFailed(tr("Aborted")); }); d->m_updateTask->start(); } @@ -680,7 +708,11 @@ void PackProfile::move(const int index, const MoveDirection direction) return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); +#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) + d->components.swapItemsAt(index, theirIndex); +#else d->components.swap(index, theirIndex); +#endif endMoveRows(); invalidateLaunchProfile(); scheduleSave(); @@ -971,19 +1003,19 @@ void PackProfile::disableInteraction(bool disable) } } -ModAPI::ModLoaderType PackProfile::getModLoader() +ModAPI::ModLoaderTypes PackProfile::getModLoaders() { - if (!getComponentVersion("net.minecraftforge").isEmpty()) - { - return ModAPI::Forge; - } - else if (!getComponentVersion("net.fabricmc.fabric-loader").isEmpty()) - { - return ModAPI::Fabric; - } - else if (!getComponentVersion("org.quiltmc.quilt-loader").isEmpty()) + ModAPI::ModLoaderTypes result = ModAPI::Unspecified; + + QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping); + + while (i.hasNext()) { - return ModAPI::Quilt; + i.next(); + Component* c = getComponent(i.key()); + if (c != nullptr && c->isEnabled()) { + result |= i.value(); + } } - return ModAPI::Unspecified; + return result; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index ab4cd5c8..918e7f7a 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -118,7 +118,7 @@ public: // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - ModAPI::ModLoaderType getModLoader(); + ModAPI::ModLoaderTypes getModLoaders(); private: void scheduleSave(); diff --git a/launcher/minecraft/ParseUtils_test.cpp b/launcher/minecraft/ParseUtils_test.cpp index fcc137e5..7721a46d 100644 --- a/launcher/minecraft/ParseUtils_test.cpp +++ b/launcher/minecraft/ParseUtils_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "minecraft/ParseUtils.h" @@ -42,4 +41,3 @@ slots: QTEST_GUILESS_MAIN(ParseUtilsTest) #include "ParseUtils_test.moc" - diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index 8ca24cc8..03f8c198 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ProfileUtils.h" #include "minecraft/VersionFilterData.h" #include "minecraft/OneSixVersionFormat.h" @@ -141,24 +176,6 @@ bool saveJsonFile(const QJsonDocument doc, const QString & filename) return true; } -VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo) -{ - QFile file(fileInfo.absoluteFilePath()); - if (!file.open(QFile::ReadOnly)) - { - auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); - return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); - } - QJsonDocument doc = QJsonDocument::fromBinaryData(file.readAll()); - file.close(); - if (doc.isNull()) - { - file.remove(); - throw JSONValidationError(QObject::tr("Unable to process the version file %1.").arg(fileInfo.fileName())); - } - return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), false); -} - void removeLwjglFromPatch(VersionFilePtr patch) { auto filter = [](QList<LibraryPtr>& libs) diff --git a/launcher/minecraft/ProfileUtils.h b/launcher/minecraft/ProfileUtils.h index 351c36cb..5b938784 100644 --- a/launcher/minecraft/ProfileUtils.h +++ b/launcher/minecraft/ProfileUtils.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "Library.h" #include "VersionFile.h" @@ -19,9 +54,6 @@ VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder) /// Save a JSON file (in any format) bool saveJsonFile(const QJsonDocument doc, const QString & filename); -/// Parse a version file in binary JSON format -VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo); - /// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. void removeLwjglFromPatch(VersionFilePtr patch); diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index 9db30ba2..a9a0f7f4 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,7 +56,7 @@ void VersionFile::applyTo(LaunchProfile *profile) // Only real Minecraft can set those. Don't let anything override them. if (isMinecraftVersion(uid)) { - profile->applyMinecraftVersion(minecraftVersion); + profile->applyMinecraftVersion(version); profile->applyMinecraftVersionType(type); // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: @@ -88,14 +89,3 @@ void VersionFile::applyTo(LaunchProfile *profile) } profile->applyProblemSeverity(getProblemSeverity()); } - -/* - auto theirVersion = profile->getMinecraftVersion(); - if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull()) - { - if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(theirVersion) == -1) - { - throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion); - } - } -*/ diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index dc756e06..dfcb43d8 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -1,16 +1,36 @@ -/* Copyright 2015-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QDir> @@ -321,7 +341,8 @@ bool World::install(const QString &to, const QString &name) if(ok && !name.isEmpty() && m_actualName != name) { - World newWorld(finalPath); + QFileInfo finalPathInfo(finalPath); + World newWorld(finalPathInfo); if(newWorld.isValid()) { newWorld.rename(name); diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 955609bf..aee7be35 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -1,16 +1,36 @@ -/* Copyright 2015-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "WorldList.h" @@ -195,7 +215,7 @@ QVariant WorldList::data(const QModelIndex &index, int role) const switch (column) { case SizeColumn: - return qVariantFromValue<qlonglong>(world.bytes()); + return QVariant::fromValue<qlonglong>(world.bytes()); default: return data(index, Qt::DisplayRole); @@ -215,7 +235,7 @@ QVariant WorldList::data(const QModelIndex &index, int role) const } case SeedRole: { - return qVariantFromValue<qlonglong>(world.seed()); + return QVariant::fromValue<qlonglong>(world.seed()); } case NameRole: { @@ -227,7 +247,7 @@ QVariant WorldList::data(const QModelIndex &index, int role) const } case SizeRole: { - return qVariantFromValue<qlonglong>(world.bytes()); + return QVariant::fromValue<qlonglong>(world.bytes()); } case IconFileRole: { @@ -301,7 +321,11 @@ public: } protected: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QVariant retrieveData(const QString &mimetype, QMetaType type) const +#else QVariant retrieveData(const QString &mimetype, QVariant::Type type) const +#endif { QList<QUrl> urls; for(auto &world: m_worlds) diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index dd9c3f8f..44f7e256 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -39,6 +39,7 @@ #include <QJsonArray> #include <QDebug> #include <QUuid> +#include <QRegularExpression> namespace { void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { @@ -451,7 +452,7 @@ void AccountData::invalidateClientToken() { if(type != AccountType::Mojang) { return; } - yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]")); + yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); } QString AccountData::profileId() const { @@ -473,7 +474,7 @@ QString AccountData::accountDisplayString() const { return userName(); } case AccountType::Offline: { - return userName(); + return QObject::tr("<Offline>"); } case AccountType::MSA: { if(xboxApiToken.extra.contains("gtg")) { diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 3422df7c..2b851e18 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -282,6 +282,10 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case Qt::DisplayRole: switch (index.column()) { + case ProfileNameColumn: { + return account->profileName(); + } + case NameColumn: return account->accountDisplayString(); @@ -300,7 +304,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const return tr("Offline", "Account status"); } case AccountState::Online: { - return tr("Online", "Account status"); + return tr("Ready", "Account status"); } case AccountState::Working: { return tr("Working", "Account status"); @@ -320,10 +324,6 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } } - case ProfileNameColumn: { - return account->profileName(); - } - case MigrationColumn: { if(account->isMSA() || account->isOffline()) { return tr("N/A", "Can Migrate?"); @@ -349,7 +349,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case Qt::CheckStateRole: switch (index.column()) { - case NameColumn: + case ProfileNameColumn: return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; } @@ -365,6 +365,8 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r case Qt::DisplayRole: switch (section) { + case ProfileNameColumn: + return tr("Username"); case NameColumn: return tr("Account"); case TypeColumn: @@ -373,8 +375,6 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r return tr("Status"); case MigrationColumn: return tr("Can Migrate?"); - case ProfileNameColumn: - return tr("Profile"); default: return QVariant(); } @@ -382,6 +382,8 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r case Qt::ToolTipRole: switch (section) { + case ProfileNameColumn: + return tr("Minecraft username associated with the account."); case NameColumn: return tr("User name of the account."); case TypeColumn: @@ -389,9 +391,7 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r case StatusColumn: return tr("Current status of the account."); case MigrationColumn: - return tr("Can this account migrate to Microsoft account?"); - case ProfileNameColumn: - return tr("Name of the Minecraft profile associated with the account."); + return tr("Can this account migrate to a Microsoft account?"); default: return QVariant(); } diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index baaf7414..8136a92e 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -58,8 +58,8 @@ public: enum VListColumns { // TODO: Add icon column. - NameColumn = 0, - ProfileNameColumn, + ProfileNameColumn = 0, + NameColumn, MigrationColumn, TypeColumn, StatusColumn, diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 49b6e46f..4118c3c5 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -79,6 +79,8 @@ QString AccountTask::getStateMessage() const bool AccountTask::changeState(AccountTaskState newState, QString reason) { m_taskState = newState; + // FIXME: virtual method invoked in constructor. + // We want that behavior, but maybe make it less weird? setStatus(getStateMessage()); switch(newState) { case AccountTaskState::STATE_CREATED: { diff --git a/launcher/minecraft/auth/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp index feface80..bb82e1e2 100644 --- a/launcher/minecraft/auth/AuthRequest.cpp +++ b/launcher/minecraft/auth/AuthRequest.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include <cassert> #include <QDebug> @@ -20,7 +55,11 @@ void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { reply_ = APPLICATION->network()->get(request_); status_ = Requesting; timedReplies_.add(new Katabasis::Reply(reply_, timeout)); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); +#else connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); +#endif connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); } @@ -31,7 +70,11 @@ void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int t status_ = Requesting; reply_ = APPLICATION->network()->post(request_, data_); timedReplies_.add(new Katabasis::Reply(reply_, timeout)); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); +#else connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); +#endif connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ec86fa5c..a5c6f542 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -40,7 +40,7 @@ #include <QUuid> #include <QJsonObject> #include <QJsonArray> -#include <QRegExp> +#include <QRegularExpression> #include <QStringList> #include <QJsonDocument> @@ -53,7 +53,7 @@ #include "flows/Offline.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); } @@ -78,7 +78,7 @@ MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username MinecraftAccountPtr account = new MinecraftAccount(); account->data.type = AccountType::Mojang; account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); return account; } @@ -97,10 +97,10 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.minecraftEntitlement.ownsMinecraft = true; account->data.minecraftEntitlement.canPlayMinecraft = true; - account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Katabasis::Validity::Certain; return account; @@ -135,6 +135,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) { m_currentTask.reset(new MojangLogin(&data, password)); connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } @@ -145,6 +146,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() { m_currentTask.reset(new MSAInteractive(&data)); connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } @@ -155,6 +157,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() { m_currentTask.reset(new OfflineLogin(&data)); connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } @@ -176,6 +179,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() { connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp index 4c6b1624..e1d33172 100644 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -9,6 +9,7 @@ YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(dat connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed); connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded); + connect(m_yggdrasil, &Task::aborted, this, &YggdrasilStep::onAuthFailed); } YggdrasilStep::~YggdrasilStep() noexcept = default; diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp index 742170fa..152485b3 100644 --- a/launcher/minecraft/launch/DirectJavaLaunch.cpp +++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp @@ -12,13 +12,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #include "DirectJavaLaunch.h" + +#include <QStandardPaths> + #include <launch/LaunchTask.h> #include <minecraft/MinecraftInstance.h> #include <FileSystem.h> #include <Commandline.h> -#include <QStandardPaths> + +#ifdef Q_OS_LINUX +#include "gamemode_client.h" +#endif DirectJavaLaunch::DirectJavaLaunch(LaunchTask *parent) : LaunchStep(parent) { @@ -50,7 +55,7 @@ void DirectJavaLaunch::executeTask() auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); - m_process.setProcessEnvironment(instance->createEnvironment()); + m_process.setProcessEnvironment(instance->createLaunchEnvironment()); // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); @@ -79,6 +84,17 @@ void DirectJavaLaunch::executeTask() { m_process.start(javaPath, args); } + +#ifdef Q_OS_LINUX + if (instance->settings()->get("EnableFeralGamemode").toBool()) + { + auto pid = m_process.processId(); + if (pid) + { + gamemode_request_start_for(pid); + } + } +#endif } void DirectJavaLaunch::on_state(LoggedProcess::State state) diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d7010355..63e4d90f 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "LauncherPartLaunch.h" @@ -23,6 +43,10 @@ #include "Commandline.h" #include "Application.h" +#ifdef Q_OS_LINUX +#include "gamemode_client.h" +#endif + LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) { auto instance = parent->instance(); @@ -71,6 +95,15 @@ bool fitsInLocal8bit(const QString & string) void LauncherPartLaunch::executeTask() { + QString jarPath = APPLICATION->getJarPath("NewLaunch.jar"); + if (jarPath.isEmpty()) + { + const char *reason = QT_TR_NOOP("Launcher library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + auto instance = m_parent->instance(); std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); @@ -81,13 +114,13 @@ void LauncherPartLaunch::executeTask() auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); - m_process.setProcessEnvironment(instance->createEnvironment()); + m_process.setProcessEnvironment(instance->createLaunchEnvironment()); // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); auto classPath = minecraftInstance->getClassPath(); - classPath.prepend(FS::PathCombine(APPLICATION->getJarsPath(), "NewLaunch.jar")); + classPath.prepend(jarPath); auto natPath = minecraftInstance->getNativePath(); #ifdef Q_OS_WIN @@ -121,7 +154,7 @@ void LauncherPartLaunch::executeTask() #else args << classPath.join(':'); #endif - args << "org.multimc.EntryPoint"; + args << "org.polymc.EntryPoint"; qDebug() << args.join(' '); @@ -146,6 +179,17 @@ void LauncherPartLaunch::executeTask() { m_process.start(javaPath, args); } + +#ifdef Q_OS_LINUX + if (instance->settings()->get("EnableFeralGamemode").toBool()) + { + auto pid = m_process.processId(); + if (pid) + { + gamemode_request_start_for(pid); + } + } +#endif } void LauncherPartLaunch::on_state(LoggedProcess::State state) diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h new file mode 100644 index 00000000..56962818 --- /dev/null +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <memory> + +#include "modplatform/packwiz/Packwiz.h" + +// launcher/minecraft/mod/Mod.h +class Mod; + +/* Abstraction file for easily changing the way metadata is stored / handled + * Needs to be a class because of -Wunused-function and no C++17 [[maybe_unused]] + * */ +class Metadata { + public: + using ModStruct = Packwiz::V1::Mod; + + static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct + { + return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); + } + + static auto create(QDir& index_dir, Mod& internal_mod) -> ModStruct + { + return Packwiz::V1::createModFormat(index_dir, internal_mod); + } + + static void update(QDir& index_dir, ModStruct& mod) + { + Packwiz::V1::updateModIndex(index_dir, mod); + } + + static void remove(QDir& index_dir, QString& mod_name) + { + Packwiz::V1::deleteModIndex(index_dir, mod_name); + } + + static auto get(QDir& index_dir, QString& mod_name) -> ModStruct + { + return Packwiz::V1::getIndexForMod(index_dir, mod_name); + } +}; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index b6bff29b..742709e3 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -1,24 +1,49 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "Mod.h" #include <QDir> #include <QString> -#include "Mod.h" -#include <QDebug> #include <FileSystem.h> +#include <QDebug> + +#include "Application.h" +#include "MetadataHandler.h" namespace { @@ -26,57 +51,67 @@ ModDetails invalidDetails; } - -Mod::Mod(const QFileInfo &file) +Mod::Mod(const QFileInfo& file) { repath(file); m_changedDateTime = file.lastModified(); } -void Mod::repath(const QFileInfo &file) +Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) + : m_file(mods_dir.absoluteFilePath(metadata.filename)) + , m_internal_id(metadata.filename) + , m_name(metadata.name) +{ + if (m_file.isDir()) { + m_type = MOD_FOLDER; + } else { + if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar")) + m_type = MOD_ZIPFILE; + else if (metadata.filename.endsWith(".litemod")) + m_type = MOD_LITEMOD; + else + m_type = MOD_SINGLEFILE; + } + + m_enabled = true; + m_changedDateTime = m_file.lastModified(); + + m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata)); +} + +void Mod::repath(const QFileInfo& file) { m_file = file; QString name_base = file.fileName(); m_type = Mod::MOD_UNKNOWN; - m_mmc_id = name_base; + m_internal_id = name_base; - if (m_file.isDir()) - { + if (m_file.isDir()) { m_type = MOD_FOLDER; m_name = name_base; - } - else if (m_file.isFile()) - { - if (name_base.endsWith(".disabled")) - { + } else if (m_file.isFile()) { + if (name_base.endsWith(".disabled")) { m_enabled = false; name_base.chop(9); - } - else - { + } else { m_enabled = true; } - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) - { + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) { m_type = MOD_ZIPFILE; name_base.chop(4); - } - else if (name_base.endsWith(".litemod")) - { + } else if (name_base.endsWith(".litemod")) { m_type = MOD_LITEMOD; name_base.chop(8); - } - else - { + } else { m_type = MOD_SINGLEFILE; } m_name = name_base; } } -bool Mod::enable(bool value) +auto Mod::enable(bool value) -> bool { if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) return false; @@ -85,67 +120,126 @@ bool Mod::enable(bool value) return false; QString path = m_file.absoluteFilePath(); - if (value) - { - QFile foo(path); + QFile file(path); + if (value) { if (!path.endsWith(".disabled")) return false; path.chop(9); - if (!foo.rename(path)) + + if (!file.rename(path)) return false; - } - else - { - QFile foo(path); + } else { path += ".disabled"; - if (!foo.rename(path)) + + if (!file.rename(path)) return false; } - repath(QFileInfo(path)); + + if (status() == ModStatus::NoMetadata) + repath(QFileInfo(path)); + m_enabled = value; return true; } -bool Mod::destroy() +void Mod::setStatus(ModStatus status) { - m_type = MOD_UNKNOWN; - return FS::deletePath(m_file.filePath()); + if (m_localDetails) { + m_localDetails->status = status; + } else { + m_temp_status = status; + } } +void Mod::setMetadata(Metadata::ModStruct* metadata) +{ + if (status() == ModStatus::NoMetadata) + setStatus(ModStatus::Installed); + if (m_localDetails) { + m_localDetails->metadata.reset(metadata); + } else { + m_temp_metadata.reset(metadata); + } +} -const ModDetails & Mod::details() const +auto Mod::destroy(QDir& index_dir) -> bool { - if(!m_localDetails) - return invalidDetails; - return *m_localDetails; -} + auto n = name(); + // FIXME: This can fail to remove the metadata if the + // "ModMetadataDisabled" setting is on, since there could + // be a name mismatch! + Metadata::remove(index_dir, n); + m_type = MOD_UNKNOWN; + return FS::deletePath(m_file.filePath()); +} -QString Mod::version() const +auto Mod::details() const -> const ModDetails& { - return details().version; + return m_localDetails ? *m_localDetails : invalidDetails; } -QString Mod::name() const +auto Mod::name() const -> QString { - auto & d = details(); - if(!d.name.isEmpty()) { - return d.name; + auto d_name = details().name; + if (!d_name.isEmpty()) { + return d_name; } return m_name; } -QString Mod::homeurl() const +auto Mod::version() const -> QString +{ + return details().version; +} + +auto Mod::homeurl() const -> QString { return details().homeurl; } -QString Mod::description() const +auto Mod::description() const -> QString { return details().description; } -QStringList Mod::authors() const +auto Mod::authors() const -> QStringList { return details().authors; } + +auto Mod::status() const -> ModStatus +{ + if (!m_localDetails) + return m_temp_status; + return details().status; +} + +auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct> +{ + if (m_localDetails) + return m_localDetails->metadata; + return m_temp_metadata; +} + +auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct> +{ + if (m_localDetails) + return m_localDetails->metadata; + return m_temp_metadata; +} + +void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details) +{ + m_resolving = false; + m_resolved = true; + m_localDetails = details; + + if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { + m_localDetails->metadata = m_temp_metadata; + if (status() == ModStatus::NoMetadata) + setStatus(ModStatus::Installed); + } + + setStatus(m_temp_status); +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 921faeb1..5f9c4684 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -1,28 +1,46 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #pragma once -#include <QFileInfo> + #include <QDateTime> +#include <QFileInfo> #include <QList> -#include <memory> #include "ModDetails.h" - - class Mod { public: @@ -32,84 +50,73 @@ public: MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). MOD_FOLDER, //!< The mod is in a folder on the filesystem. - MOD_LITEMOD, //!< The mod is a litemod + MOD_LITEMOD, //!< The mod is a litemod }; Mod() = default; Mod(const QFileInfo &file); + explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); - QFileInfo filename() const - { - return m_file; - } - QString mmc_id() const - { - return m_mmc_id; - } - ModType type() const - { - return m_type; - } - bool valid() - { - return m_type != MOD_UNKNOWN; - } + auto fileinfo() const -> QFileInfo { return m_file; } + auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; } + auto internal_id() const -> QString { return m_internal_id; } + auto type() const -> ModType { return m_type; } + auto enabled() const -> bool { return m_enabled; } - QDateTime dateTimeChanged() const - { - return m_changedDateTime; - } + auto valid() const -> bool { return m_type != MOD_UNKNOWN; } - bool enabled() const - { - return m_enabled; - } + auto details() const -> const ModDetails&; + auto name() const -> QString; + auto version() const -> QString; + auto homeurl() const -> QString; + auto description() const -> QString; + auto authors() const -> QStringList; + auto status() const -> ModStatus; - const ModDetails &details() const; + auto metadata() -> std::shared_ptr<Metadata::ModStruct>; + auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>; - QString name() const; - QString version() const; - QString homeurl() const; - QString description() const; - QStringList authors() const; + void setStatus(ModStatus status); + void setMetadata(Metadata::ModStruct* metadata); - bool enable(bool value); + auto enable(bool value) -> bool; // delete all the files of this mod - bool destroy(); + auto destroy(QDir& index_dir) -> bool; // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); - bool shouldResolve() { - return !m_resolving && !m_resolved; - } - bool isResolving() { - return m_resolving; - } - int resolutionTicket() - { - return m_resolutionTicket; - } + auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; } + auto isResolving() const -> bool { return m_resolving; } + auto resolutionTicket() const -> int { return m_resolutionTicket; } + void setResolving(bool resolving, int resolutionTicket) { m_resolving = resolving; m_resolutionTicket = resolutionTicket; } - void finishResolvingWithDetails(std::shared_ptr<ModDetails> details){ - m_resolving = false; - m_resolved = true; - m_localDetails = details; - } + void finishResolvingWithDetails(std::shared_ptr<ModDetails> details); protected: QFileInfo m_file; QDateTime m_changedDateTime; - QString m_mmc_id; + + QString m_internal_id; + /* Name as reported via the file name */ QString m_name; + ModType m_type = MOD_UNKNOWN; + + /* If the mod has metadata, this will be filled in the constructor, and passed to + * the ModDetails when calling finishResolvingWithDetails */ + std::shared_ptr<Metadata::ModStruct> m_temp_metadata; + + /* Set the mod status while it doesn't have local details just yet */ + ModStatus m_temp_status = ModStatus::NotInstalled; + + std::shared_ptr<ModDetails> m_localDetails; + bool m_enabled = true; bool m_resolving = false; bool m_resolved = false; int m_resolutionTicket = 0; - ModType m_type = MOD_UNKNOWN; - std::shared_ptr<ModDetails> m_localDetails; }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 6ab4aee7..3e0a7ab0 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -1,17 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + #pragma once +#include <memory> + #include <QString> #include <QStringList> +#include "minecraft/mod/MetadataHandler.h" + +enum class ModStatus { + Installed, // Both JAR and Metadata are present + NotInstalled, // Only the Metadata is present + NoMetadata, // Only the JAR is present +}; + struct ModDetails { + /* Mod ID as defined in the ModLoader-specific metadata */ QString mod_id; + + /* Human-readable name */ QString name; + + /* Human-readable mod version */ QString version; + + /* Human-readable minecraft version */ QString mcversion; + + /* URL for mod's home page */ QString homeurl; - QString updateurl; + + /* Human-readable description */ QString description; + + /* List of the author's names */ QStringList authors; - QString credits; + + /* Installation status of the mod */ + ModStatus status; + + /* Metadata information, if any */ + std::shared_ptr<Metadata::ModStruct> metadata; }; diff --git a/launcher/minecraft/mod/ModFolderLoadTask.cpp b/launcher/minecraft/mod/ModFolderLoadTask.cpp deleted file mode 100644 index 88349877..00000000 --- a/launcher/minecraft/mod/ModFolderLoadTask.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "ModFolderLoadTask.h" -#include <QDebug> - -ModFolderLoadTask::ModFolderLoadTask(QDir dir) : - m_dir(dir), m_result(new Result()) -{ -} - -void ModFolderLoadTask::run() -{ - m_dir.refresh(); - for (auto entry : m_dir.entryInfoList()) - { - Mod m(entry); - m_result->mods[m.mmc_id()] = m; - } - emit succeeded(); -} diff --git a/launcher/minecraft/mod/ModFolderLoadTask.h b/launcher/minecraft/mod/ModFolderLoadTask.h deleted file mode 100644 index 8d720e65..00000000 --- a/launcher/minecraft/mod/ModFolderLoadTask.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include <QRunnable> -#include <QObject> -#include <QDir> -#include <QMap> -#include "Mod.h" -#include <memory> - -class ModFolderLoadTask : public QObject, public QRunnable -{ - Q_OBJECT -public: - struct Result { - QMap<QString, Mod> mods; - }; - using ResultPtr = std::shared_ptr<Result>; - ResultPtr result() const { - return m_result; - } - -public: - ModFolderLoadTask(QDir dir); - void run(); -signals: - void succeeded(); -private: - QDir m_dir; - ResultPtr m_result; -}; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index f0c53c39..5ee08cbf 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -1,32 +1,55 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #include "ModFolderModel.h" + #include <FileSystem.h> +#include <QDebug> +#include <QFileSystemWatcher> #include <QMimeData> -#include <QUrl> -#include <QUuid> #include <QString> -#include <QFileSystemWatcher> -#include <QDebug> -#include "ModFolderLoadTask.h" #include <QThreadPool> +#include <QUrl> +#include <QUuid> #include <algorithm> -#include "LocalModParseTask.h" -ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) +#include "minecraft/mod/tasks/LocalModParseTask.h" +#include "minecraft/mod/tasks/ModFolderLoadTask.h" + +ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); @@ -79,19 +102,31 @@ bool ModFolderModel::update() return true; } - auto task = new ModFolderLoadTask(m_dir); + auto index_dir = indexDir(); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); + threadPool->start(task); return true; } void ModFolderModel::finishUpdate() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto currentList = modsIndex.keys(); + QSet<QString> currentSet(currentList.begin(), currentList.end()); + auto & newMods = m_update->mods; + auto newList = newMods.keys(); + QSet<QString> newSet(newList.begin(), newList.end()); +#else QSet<QString> currentSet = modsIndex.keys().toSet(); auto & newMods = m_update->mods; QSet<QString> newSet = newMods.keys().toSet(); +#endif // see if the kept mods changed in some way { @@ -140,12 +175,16 @@ void ModFolderModel::finishUpdate() { QSet<QString> added = newSet; added.subtract(currentSet); - beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); - for(auto & addedMod: added) { - mods.append(newMods[addedMod]); - resolveMod(mods.last()); + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (added.size() > 0) { + beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); + for (auto& addedMod : added) { + mods.append(newMods[addedMod]); + resolveMod(mods.last()); + } + endInsertRows(); } - endInsertRows(); } // update index @@ -153,7 +192,7 @@ void ModFolderModel::finishUpdate() modsIndex.clear(); int idx = 0; for(auto & mod: mods) { - modsIndex[mod.mmc_id()] = idx; + modsIndex[mod.internal_id()] = idx; idx++; } } @@ -174,9 +213,9 @@ void ModFolderModel::resolveMod(Mod& m) return; } - auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo()); auto result = task->result(); - result->id = m.mmc_id(); + result->id = m.internal_id(); activeTickets.insert(nextResolutionTicket, result); m.setResolving(true, nextResolutionTicket); nextResolutionTicket++; @@ -278,7 +317,8 @@ bool ModFolderModel::installMod(const QString &filename) return false; } FS::updateTimestamp(newpath); - installedMod.repath(newpath); + QFileInfo newpathInfo(newpath); + installedMod.repath(newpathInfo); update(); return true; } @@ -296,7 +336,8 @@ bool ModFolderModel::installMod(const QString &filename) qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed."; return false; } - installedMod.repath(newpath); + QFileInfo newpathInfo(newpath); + installedMod.repath(newpathInfo); update(); return true; } @@ -333,8 +374,12 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) for (auto i: indexes) { + if(i.column() != 0) { + continue; + } Mod &m = mods[i.row()]; - m.destroy(); + auto index_dir = indexDir(); + m.destroy(index_dir); } return true; } @@ -381,7 +426,7 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: - return mods[row].mmc_id(); + return mods[row].internal_id(); case Qt::CheckStateRole: switch (column) @@ -436,11 +481,11 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio } // preserve the row, but change its ID - auto oldId = mod.mmc_id(); + auto oldId = mod.internal_id(); if(!mod.enable(!mod.enabled())) { return false; } - auto newId = mod.mmc_id(); + auto newId = mod.internal_id(); if(modsIndex.contains(newId)) { // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled // But is it necessary? diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 62c504df..24b4d358 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -1,17 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #pragma once @@ -24,8 +45,8 @@ #include "Mod.h" -#include "ModFolderLoadTask.h" -#include "LocalModParseTask.h" +#include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalModParseTask.h" class LegacyInstance; class BaseInstance; @@ -52,7 +73,7 @@ public: Enable, Toggle }; - ModFolderModel(const QString &dir); + ModFolderModel(const QString &dir, bool is_indexed = false); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; @@ -108,11 +129,16 @@ public: bool isValid(); - QDir dir() + QDir& dir() { return m_dir; } + QDir indexDir() + { + return { QString("%1/.index").arg(dir().absolutePath()) }; + } + const QList<Mod> & allMods() { return mods; @@ -141,6 +167,7 @@ protected: bool scheduled_update = false; bool interaction_disabled = false; QDir m_dir; + bool m_is_indexed; QMap<QString, int> modsIndex; QMap<int, LocalModParseTask::ResultPtr> activeTickets; int nextResolutionTicket = 0; diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp index 76f16ed5..b4d37ce5 100644 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ b/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -1,7 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #include <QTest> #include <QTemporaryDir> -#include "TestUtil.h" #include "FileSystem.h" #include "minecraft/mod/ModFolderModel.h" @@ -16,7 +49,7 @@ slots: void test_1178() { // source - QString source = QFINDTESTDATA("data/test_folder"); + QString source = QFINDTESTDATA("testdata/test_folder"); // sanity check QVERIFY(!source.endsWith('/')); @@ -32,8 +65,11 @@ slots: { QString folder = source; QTemporaryDir tempDir; - ModFolderModel m(tempDir.path()); + QEventLoop loop; + ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); m.installMod(folder); + loop.exec(); verify(tempDir.path()); } @@ -41,8 +77,11 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; - ModFolderModel m(tempDir.path()); + QEventLoop loop; + ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); m.installMod(folder); + loop.exec(); verify(tempDir.path()); } } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index f3d7f566..276804ed 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + #include "ResourcePackFolderModel.h" ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index d5956da1..e3a22219 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + #include "TexturePackFolderModel.h" TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index a7bec5ae..3354732b 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -35,7 +35,6 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) details->name = name; } details->version = firstObj.value("version").toString(); - details->updateurl = firstObj.value("updateUrl").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); if(!homeurl.isEmpty()) { @@ -57,7 +56,6 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) { details->authors.append(author.toString()); } - details->credits = firstObj.value("credits").toString(); return details; }; QJsonParseError jsonError; @@ -168,27 +166,9 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents) } if(!authors.isEmpty()) { - // author information is stored as a string now, not a list details->authors.append(authors); } - // is credits even used anywhere? including this for completion/parity with old data version - toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); - QString credits = ""; - if(creditsDatum.ok) - { - authors = creditsDatum.u.s; - free(creditsDatum.u.s); - } - else - { - creditsDatum = toml_string_in(tomlModsTable0, "credits"); - if(creditsDatum.ok) - { - credits = creditsDatum.u.s; - free(creditsDatum.u.s); - } - } - details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); QString homeurl = ""; if(homeurlDatum.ok) diff --git a/launcher/minecraft/mod/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 0f119ba6..ed92394c 100644 --- a/launcher/minecraft/mod/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -1,9 +1,11 @@ #pragma once -#include <QRunnable> + #include <QDebug> #include <QObject> -#include "Mod.h" -#include "ModDetails.h" +#include <QRunnable> + +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ModDetails.h" class LocalModParseTask : public QObject, public QRunnable { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp new file mode 100644 index 00000000..1bdecb8c --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "LocalModUpdateTask.h" + +#include "Application.h" +#include "FileSystem.h" +#include "minecraft/mod/MetadataHandler.h" + +#ifdef Q_OS_WIN32 +#include <windows.h> +#endif + +LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version) + : m_index_dir(index_dir), m_mod(mod), m_mod_version(mod_version) +{ + // Ensure a '.index' folder exists in the mods folder, and create it if it does not + if (!FS::ensureFolderPathExists(index_dir.path())) { + emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); + } + +#ifdef Q_OS_WIN32 + SetFileAttributesA(index_dir.path().toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif +} + +void LocalModUpdateTask::executeTask() +{ + setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + + auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); + Metadata::update(m_index_dir, pw_mod); + + emitSucceeded(); +} + +auto LocalModUpdateTask::abort() -> bool +{ + emitAborted(); + return true; +} diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h new file mode 100644 index 00000000..2db183e0 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <QDir> + +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class LocalModUpdateTask : public Task { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<LocalModUpdateTask>; + + explicit LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); + + auto canAbort() const -> bool override { return true; } + auto abort() -> bool override; + + protected slots: + //! Entry point for tasks. + void executeTask() override; + + private: + QDir m_index_dir; + ModPlatform::IndexedPack& m_mod; + ModPlatform::IndexedVersion& m_mod_version; +}; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp new file mode 100644 index 00000000..276414e4 --- /dev/null +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "ModFolderLoadTask.h" + +#include "Application.h" +#include "minecraft/mod/MetadataHandler.h" + +ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed) + : m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_result(new Result()) +{} + +void ModFolderLoadTask::run() +{ + if (m_is_indexed) { + // Read metadata first + getFromMetadata(); + } + + // Read JAR files that don't have metadata + m_mods_dir.refresh(); + for (auto entry : m_mods_dir.entryInfoList()) { + Mod mod(entry); + + if (mod.enabled()) { + if (m_result->mods.contains(mod.internal_id())) { + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + } + else { + m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } + } + else { + QString chopped_id = mod.internal_id().chopped(9); + if (m_result->mods.contains(chopped_id)) { + m_result->mods[mod.internal_id()] = mod; + + auto metadata = m_result->mods[chopped_id].metadata(); + if (metadata) { + mod.setMetadata(new Metadata::ModStruct(*metadata)); + + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + m_result->mods.remove(chopped_id); + } + } + else { + m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } + } + } + + emit succeeded(); +} + +void ModFolderLoadTask::getFromMetadata() +{ + m_index_dir.refresh(); + for (auto entry : m_index_dir.entryList(QDir::Files)) { + auto metadata = Metadata::get(m_index_dir, entry); + + if(!metadata.isValid()){ + return; + } + + Mod mod(m_mods_dir, metadata); + mod.setStatus(ModStatus::NotInstalled); + m_result->mods[mod.internal_id()] = mod; + } +} diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h new file mode 100644 index 00000000..088f873e --- /dev/null +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#pragma once + +#include <QDir> +#include <QMap> +#include <QObject> +#include <QRunnable> +#include <memory> +#include "minecraft/mod/Mod.h" + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QMap<QString, Mod> mods; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + +public: + ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed); + void run(); +signals: + void succeeded(); + +private: + void getFromMetadata(); + +private: + QDir& m_mods_dir, m_index_dir; + bool m_is_indexed; + ResultPtr m_result; +}; diff --git a/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt b/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt @@ -0,0 +1 @@ + diff --git a/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta b/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta new file mode 100644 index 00000000..67ee0434 --- /dev/null +++ b/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 1, + "description": "Some resource pack maybe" + } +} diff --git a/launcher/minecraft/mod/testdata/test_folder/pack.nfo b/launcher/minecraft/mod/testdata/test_folder/pack.nfo new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/launcher/minecraft/mod/testdata/test_folder/pack.nfo @@ -0,0 +1 @@ + diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index e49c166a..c73a11b6 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "CapeChange.h" #include <QNetworkRequest> @@ -34,7 +69,11 @@ void CapeChange::clearCape() { m_reply = shared_qobject_ptr<QNetworkReply>(rep); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); +#else connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); +#endif connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); } diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp index cce8364e..921bd094 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/services/SkinDelete.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "SkinDelete.h" #include <QNetworkRequest> @@ -19,7 +54,11 @@ void SkinDelete::executeTask() setStatus(tr("Deleting skin")); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); +#else connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); +#endif connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); } diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index 7c2e8337..c7987875 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "SkinUpload.h" #include <QNetworkRequest> @@ -44,7 +79,11 @@ void SkinUpload::executeTask() setStatus(tr("Uploading skin")); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); +#else connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); +#endif connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); } diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index c4bddb08..dd246665 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -43,6 +43,7 @@ void AssetUpdateTask::executeTask() connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); + connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); qDebug() << m_inst->name() << ": Starting asset index download"; @@ -80,6 +81,7 @@ void AssetUpdateTask::assetIndexFinished() downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); + connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); downloadJob->start(); return; diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp index 58141991..b6238ce9 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -72,6 +72,7 @@ void FMLLibrariesTask::executeTask() connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); + connect(dljob, &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress); downloadJob.reset(dljob); downloadJob->start(); diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 26679110..aa2bf407 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -68,6 +68,7 @@ void LibrariesTask::executeTask() connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); + connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); downloadJob->start(); } diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 8e6cd45c..cf116353 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -1,12 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> #include <QList> +#include <list> #include "Version.h" namespace ModPlatform { class ListModel; +struct IndexedPack; } class ModAPI { @@ -16,24 +53,32 @@ class ModAPI { public: virtual ~ModAPI() = default; - // https://docs.curseforge.com/?http#tocS_ModLoaderType - enum ModLoaderType { Unspecified = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4, Quilt = 5 }; + enum ModLoaderType { + Unspecified = 0, + Forge = 1 << 0, + Cauldron = 1 << 1, + LiteLoader = 1 << 2, + Fabric = 1 << 3, + Quilt = 1 << 4 + }; + Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) struct SearchArgs { int offset; QString search; QString sorting; - ModLoaderType mod_loader; + ModLoaderTypes loaders; std::list<Version> versions; }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; + virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; struct VersionSearchArgs { QString addonId; std::list<Version> mcVersions; - ModLoaderType loader; + ModLoaderTypes loaders; }; virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; @@ -61,7 +106,7 @@ class ModAPI { { QString s; for(auto& ver : mcVersions){ - s += QString("%1,").arg(ver.toString()); + s += QString("\"%1\",").arg(ver.toString()); } s.remove(s.length() - 1, 1); //remove last comma return s; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp new file mode 100644 index 00000000..3c4b7887 --- /dev/null +++ b/launcher/modplatform/ModIndex.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "modplatform/ModIndex.h" + +#include <QCryptographicHash> + +namespace ModPlatform { + +auto ProviderCapabilities::name(Provider p) -> const char* +{ + switch (p) { + case Provider::MODRINTH: + return "modrinth"; + case Provider::FLAME: + return "curseforge"; + } + return {}; +} +auto ProviderCapabilities::readableName(Provider p) -> QString +{ + switch (p) { + case Provider::MODRINTH: + return "Modrinth"; + case Provider::FLAME: + return "CurseForge"; + } + return {}; +} +auto ProviderCapabilities::hashType(Provider p) -> QStringList +{ + switch (p) { + case Provider::MODRINTH: + return { "sha512", "sha1" }; + case Provider::FLAME: + // Try newer formats first, fall back to old format + return { "sha1", "md5", "murmur2" }; + } + return {}; +} +auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray +{ + switch (p) { + case Provider::MODRINTH: { + // NOTE: Data is the result of reading the entire JAR file! + + // If 'type' was specified, we use that + if (!type.isEmpty() && hashType(p).contains(type)) { + if (type == "sha512") + return QCryptographicHash::hash(data, QCryptographicHash::Sha512); + else if (type == "sha1") + return QCryptographicHash::hash(data, QCryptographicHash::Sha1); + } + + return QCryptographicHash::hash(data, QCryptographicHash::Sha512); + } + case Provider::FLAME: + // If 'type' was specified, we use that + if (!type.isEmpty() && hashType(p).contains(type)) { + if(type == "sha1") + return QCryptographicHash::hash(data, QCryptographicHash::Sha1); + else if (type == "md5") + return QCryptographicHash::hash(data, QCryptographicHash::Md5); + } + + break; + } + return {}; +} + +} // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7e1cf254..c27643af 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + #pragma once #include <QList> @@ -8,11 +26,30 @@ namespace ModPlatform { +enum class Provider { + MODRINTH, + FLAME +}; + +class ProviderCapabilities { + public: + auto name(Provider) -> const char*; + auto readableName(Provider) -> QString; + auto hashType(Provider) -> QStringList; + auto hash(Provider, QByteArray&, QString type = "") -> QByteArray; +}; + struct ModpackAuthor { QString name; QString url; }; +struct DonationData { + QString id; + QString platform; + QString url; +}; + struct IndexedVersion { QVariant addonId; QVariant fileId; @@ -22,10 +59,22 @@ struct IndexedVersion { QString date; QString fileName; QVector<QString> loaders = {}; + QString hash_type; + QString hash; +}; + +struct ExtraPackData { + QList<DonationData> donate; + + QString issuesUrl; + QString sourceUrl; + QString wikiUrl; + QString discordUrl; }; struct IndexedPack { QVariant addonId; + Provider provider; QString name; QString description; QList<ModpackAuthor> authors; @@ -35,8 +84,13 @@ struct IndexedPack { bool versionsLoaded = false; QVector<IndexedVersion> versions; + + // Don't load by default, since some modplatform don't have that info + bool extraDataLoaded = true; + ExtraPackData extraData; }; } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) +Q_DECLARE_METATYPE(ModPlatform::Provider) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 9dcb3504..0ed0ad29 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -1,23 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ATLPackInstallTask.h" -#include <QtConcurrent/QtConcurrent> +#include <QtConcurrent> #include <quazip/quazip.h> @@ -39,10 +58,13 @@ namespace ATLauncher { -PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) +static Meta::VersionPtr getComponentVersion(const QString& uid, const 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 +82,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(); @@ -74,14 +96,13 @@ void PackInstallTask::onDownloadSucceeded() qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); jobPtr.reset(); - QJsonParseError parse_error; + QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } - auto obj = doc.object(); ATLauncher::PackVersion version; @@ -96,19 +117,15 @@ void PackInstallTask::onDownloadSucceeded() } m_version = version; - auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); - if(!vlist) - { - emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft")); - return; - } + // Display install message if one exists + if (!m_version.messages.install.isEmpty()) + m_support->displayMessage(m_version.messages.install); - auto ver = vlist->getVersion(m_version.minecraft); + auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { - emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft)); + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); return; } - ver->load(Net::Mode::Online); minecraftVersion = ver; if(m_version.noConfigs) { @@ -303,9 +320,50 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto f = std::make_shared<VersionFile>(); - f->name = m_pack + " " + m_version_name + " (libraries)"; + f->name = m_pack_name + " " + m_version_name + " (libraries)"; + + const static QMap<QString, QString> liteLoaderMap = { + { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, + { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, + { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, + { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, + { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, + { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, + { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" }, + { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" }, + { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" }, + { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" }, + { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" }, + { "6e9028816027f53957bd8fcdfabae064", "1.8" }, + { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" }, + { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" }, + { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" }, + { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" }, + { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" }, + { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" }, + { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" }, + { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" }, + { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" }, + { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" }, + { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" }, + { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" }, + { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" }, + { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" }, + { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" }, + { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" }, + { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" } + }; for(const auto & lib : m_version.libraries) { + // If the library is LiteLoader, we need to ignore it and handle it separately. + if (liteLoaderMap.contains(lib.md5)) { + auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); + if (ver) { + componentsToInstall.insert("com.mumfrey.liteloader", ver); + continue; + } + } + auto libName = detectLibrary(lib); GradleSpecifier libSpecifier(libName); @@ -357,7 +415,31 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile) { - if(m_version.mainClass == QString() && m_version.extraArguments == QString()) { + if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { + return true; + } + + auto mainClass = m_version.mainClass.mainClass; + auto extraArguments = m_version.extraArguments.arguments; + + auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); + auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty(); + if (hasMainClassDepends || hasExtraArgumentsDepends) { + QSet<QString> mods; + for (const auto& item : m_version.mods) { + mods.insert(item.name); + } + + if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) { + mainClass = ""; + } + + if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) { + extraArguments = ""; + } + } + + if (mainClass.isEmpty() && extraArguments.isEmpty()) { return true; } @@ -384,13 +466,13 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< } auto f = std::make_shared<VersionFile>(); - f->name = m_pack + " " + m_version_name; - if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) { - f->mainClass = m_version.mainClass; + f->name = m_pack_name + " " + m_version_name; + if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { + f->mainClass = mainClass; } // Parse out tweakers - auto args = m_version.extraArguments.split(" "); + auto args = extraArguments.split(" "); QString previous; for(auto arg : args) { if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") { @@ -426,9 +508,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); @@ -475,7 +557,11 @@ void PackInstallTask::extractConfigs() return; } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); +#else m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); +#endif connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [&]() { downloadMods(); @@ -502,7 +588,7 @@ void PackInstallTask::downloadMods() QVector<QString> 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...")); @@ -574,19 +660,12 @@ void PackInstallTask::downloadMods() jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); - qDebug() << "Will download" << url << "to" << path; - modsToCopy[entry->getFullPath()] = path; if(mod.type == ModType::Forge) { - auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); - if(vlist) - { - auto ver = vlist->getVersion(mod.version); - if(ver) { - ver->load(Net::Mode::Online); - componentsToInstall.insert("net.minecraftforge", ver); - continue; - } + auto ver = getComponentVersion("net.minecraftforge", mod.version); + if (ver) { + componentsToInstall.insert("net.minecraftforge", ver); + continue; } qDebug() << "Jarmod: " + path; @@ -597,6 +676,10 @@ void PackInstallTask::downloadMods() qDebug() << "Jarmod: " + path; jarmods.push_back(path); } + + // Download after Forge handling, to avoid downloading Forge twice. + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; } } @@ -623,7 +706,11 @@ void PackInstallTask::onModsDownloaded() { jobPtr.reset(); if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy); +#else m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); +#endif connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]() { @@ -675,7 +762,7 @@ bool PackInstallTask::extractMods( QString folderToExtract = ""; if(mod.type == ModType::Extract) { folderToExtract = mod.extractFolder; - folderToExtract.remove(QRegExp("^/")); + folderToExtract.remove(QRegularExpression("^/")); } qDebug() << "Extracting " + mod.file + " to " + extractToDir; @@ -703,6 +790,17 @@ bool PackInstallTask::extractMods( for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { auto &from = iter.key(); auto &to = iter.value(); + + // If the file already exists, assume the mod is the correct copy - and remove + // the copy from the Configs.zip + QFileInfo fileInfo(to); + if (fileInfo.exists()) { + if (!QFile::remove(to)) { + qWarning() << "Failed to delete" << to; + return false; + } + } + FS::copy fileCopyOperation(from, to); if(!fileCopyOperation()) { qWarning() << "Failed to copy" << from << "to" << to; @@ -740,14 +838,14 @@ void PackInstallTask::install() auto version = getVersionForLoader("net.minecraftforge"); if(version == Q_NULLPTR) return; - components->setComponentVersion("net.minecraftforge", version, true); + components->setComponentVersion("net.minecraftforge", version); } else if(m_version.loader.type == QString("fabric")) { auto version = getVersionForLoader("net.fabricmc.fabric-loader"); if(version == Q_NULLPTR) return; - components->setComponentVersion("net.fabricmc.fabric-loader", version, true); + components->setComponentVersion("net.fabricmc.fabric-loader", version); } else if(m_version.loader.type != QString()) { @@ -773,10 +871,30 @@ 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(); emitSucceeded(); } +static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version) +{ + auto vlist = APPLICATION->metadataIndex()->get(uid); + if (!vlist) + return {}; + + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); + + auto ver = vlist->getVersion(version); + if (!ver) + return {}; + + if (!ver->isLoaded()) + ver->load(Net::Mode::Online); + + return ver; +} + } diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 783ec19b..f55873e9 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -37,7 +56,7 @@ public: /** * Requests a user interaction to select which optional mods should be installed. */ - virtual QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0; + virtual QVector<QString> chooseOptionalMods(PackVersion version, QVector<ATLauncher::VersionMod> mods) = 0; /** * Requests a user interaction to select a component version from a given version list @@ -45,6 +64,10 @@ 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 +75,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 +117,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 40be6d53..3af02a09 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ATLPackManifest.h" @@ -178,6 +197,8 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.depends.append(Json::requireString(depends)); } } + p.colour = Json::ensureString(obj, QString("colour"), ""); + p.warning = Json::ensureString(obj, QString("warning"), ""); p.client = Json::ensureBoolean(obj, QString("client"), false); @@ -185,6 +206,24 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.effectively_hidden = p.hidden || p.library; } +static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) +{ + m.install = Json::ensureString(obj, "install", ""); + m.update = Json::ensureString(obj, "update", ""); +} + +static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) +{ + m.mainClass = Json::ensureString(obj, "mainClass", ""); + m.depends = Json::ensureString(obj, "depends", ""); +} + +static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) +{ + a.arguments = Json::ensureString(obj, "arguments", ""); + a.depends = Json::ensureString(obj, "depends", ""); +} + void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) { v.version = Json::requireString(obj, "version"); @@ -193,12 +232,12 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) if(obj.contains("mainClass")) { auto main = Json::requireObject(obj, "mainClass"); - v.mainClass = Json::ensureString(main, "mainClass", ""); + loadVersionMainClass(v.mainClass, main); } if(obj.contains("extraArguments")) { auto arguments = Json::requireObject(obj, "extraArguments"); - v.extraArguments = Json::ensureString(arguments, "arguments", ""); + loadVersionExtraArguments(v.extraArguments, arguments); } if(obj.contains("loader")) { @@ -232,4 +271,17 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) auto configsObj = Json::requireObject(obj, "configs"); loadVersionConfigs(v.configs, configsObj); } + + auto colourObj = Json::ensureObject(obj, "colours"); + for (const auto &key : colourObj.keys()) { + v.colours[key] = Json::requireString(colourObj.value(key), "colour"); + } + + auto warningsObj = Json::ensureObject(obj, "warnings"); + for (const auto &key : warningsObj.keys()) { + v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); + } + + auto messages = Json::ensureObject(obj, "messages"); + loadVersionMessages(v.messages, messages); } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 673f2f8b..43510c50 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -1,24 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <QJsonObject> +#include <QMap> #include <QString> #include <QVector> -#include <QJsonObject> namespace ATLauncher { @@ -109,6 +129,8 @@ struct VersionMod bool library; QString group; QVector<QString> depends; + QString colour; + QString warning; bool client; @@ -122,18 +144,40 @@ struct VersionConfigs QString sha1; }; +struct VersionMessages +{ + QString install; + QString update; +}; + +struct PackVersionMainClass +{ + QString mainClass; + QString depends; +}; + +struct PackVersionExtraArguments +{ + QString arguments; + QString depends; +}; + struct PackVersion { QString version; QString minecraft; bool noConfigs; - QString mainClass; - QString extraArguments; + PackVersionMainClass mainClass; + PackVersionExtraArguments extraArguments; VersionLoader loader; QVector<VersionLibrary> libraries; QVector<VersionMod> mods; VersionConfigs configs; + + QMap<QString, QString> colours; + QMap<QString, QString> warnings; + VersionMessages messages; }; void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 95924a68..c1f56658 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,49 +1,129 @@ #include "FileResolvingTask.h" + #include "Json.h" +#include "net/Upload.h" -Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess) +Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess) : m_network(network), m_toProcess(toProcess) {} void Flame::FileResolvingTask::executeTask() { setStatus(tr("Resolving mod IDs...")); - setProgress(0, m_toProcess.files.size()); + setProgress(0, 3); m_dljob = new NetJob("Mod id resolver", m_network); - results.resize(m_toProcess.files.size()); - int index = 0; - for (auto& file : m_toProcess.files) { - auto projectIdStr = QString::number(file.projectId); - auto fileIdStr = QString::number(file.fileId); - QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr); - auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]); - m_dljob->addNetAction(dl); - index++; - } + result.reset(new QByteArray()); + //build json data to send + QJsonObject object; + + object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { + l.push_back(s.fileId); + return l; + })); + QByteArray data = Json::toText(object); + auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data); + m_dljob->addNetAction(dl); connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished); m_dljob->start(); } void Flame::FileResolvingTask::netJobFinished() { - bool failed = false; + setProgress(1, 3); int index = 0; - for (auto& bytes : results) { - auto& out = m_toProcess.files[index]; + // job to check modrinth for blocked projects + auto job = new NetJob("Modrinth check", m_network); + blockedProjects = QMap<File *,QByteArray *>(); + auto doc = Json::requireDocument(*result); + auto array = Json::requireArray(doc.object()["data"]); + for (QJsonValueRef file : array) { + auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); + auto& out = m_toProcess.files[fileid]; try { - failed &= (!out.parseFromBytes(bytes)); + out.parseFromObject(Json::requireObject(file)); } catch (const JSONValidationError& e) { - qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; - qCritical() << e.cause(); - qCritical() << "JSON:"; - qCritical() << bytes; - failed = true; + qDebug() << "Blocked mod on curseforge" << out.fileName; + auto hash = out.hash; + if(!hash.isEmpty()) { + auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); + auto output = new QByteArray(); + auto dl = Net::Download::makeByteArray(QUrl(url), output); + QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { + out.resolved = true; + }); + + job->addNetAction(dl); + blockedProjects.insert(&out, output); + } } index++; } - if (!failed) { - emitSucceeded(); + connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished); + + job->start(); +} + +void Flame::FileResolvingTask::modrinthCheckFinished() { + setProgress(2, 3); + qDebug() << "Finished with blocked mods : " << blockedProjects.size(); + + for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) { + auto &out = *it; + auto bytes = blockedProjects[out]; + if (!out->resolved) { + delete bytes; + continue; + } + QJsonDocument doc = QJsonDocument::fromJson(*bytes); + auto obj = doc.object(); + auto array = Json::requireArray(obj,"files"); + for (auto file: array) { + auto fileObj = Json::requireObject(file); + auto primary = Json::requireBoolean(fileObj,"primary"); + if (primary) { + out->url = Json::requireUrl(fileObj,"url"); + qDebug() << "Found alternative on modrinth " << out->fileName; + break; + } + } + delete bytes; + } + //copy to an output list and filter out projects found on modrinth + auto block = new QList<File *>(); + auto it = blockedProjects.keys(); + std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) { + return !f->resolved; + }); + //Display not found mods early + if (!block->empty()) { + //blocked mods found, we need the slug for displaying.... we need another job :D ! + auto slugJob = new NetJob("Slug Job", m_network); + auto slugs = QVector<QByteArray>(block->size()); + auto index = 0; + for (auto fileInfo: *block) { + auto projectId = fileInfo->projectId; + slugs[index] = QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); + auto dl = Net::Download::makeByteArray(url, &slugs[index]); + slugJob->addNetAction(dl); + index++; + } + connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() { + slugJob->deleteLater(); + auto index = 0; + for (const auto &slugResult: slugs) { + auto json = QJsonDocument::fromJson(slugResult); + auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"), + "websiteUrl"); + auto mod = block->at(index); + auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId)); + mod->websiteUrl = link; + index++; + } + emitSucceeded(); + }); + slugJob->start(); } else { - emitFailed(tr("Some mod ID resolving tasks failed.")); + emitSucceeded(); } } diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index 5e5adcd7..87981f0a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -10,7 +10,7 @@ class FileResolvingTask : public Task { Q_OBJECT public: - explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess); + explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess); virtual ~FileResolvingTask() {}; const Flame::Manifest &getResults() const @@ -27,7 +27,11 @@ protected slots: private: /* data */ shared_qobject_ptr<QNetworkAccessManager> m_network; Flame::Manifest m_toProcess; - QVector<QByteArray> results; + std::shared_ptr<QByteArray> result; NetJob::Ptr m_dljob; + + void modrinthCheckFinished(); + + QMap<File *, QByteArray *> blockedProjects; }; } diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 61628e60..aea76ff1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,5 +1,6 @@ #pragma once +#include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" class FlameAPI : public NetworkModAPI { @@ -37,14 +38,19 @@ class FlameAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(getSortFieldInt(args.sorting)) - .arg(getMappedModLoader(args.mod_loader)) + .arg(getMappedModLoader(args.loaders)) .arg(gameVersionStr); }; + inline auto getModInfoURL(QString& id) const -> QString override + { + return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; - QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader)); + QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders)); return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3") .arg(args.addonId) @@ -53,11 +59,16 @@ class FlameAPI : public NetworkModAPI { }; public: - static auto getMappedModLoader(const ModLoaderType type) -> const ModLoaderType + static auto getMappedModLoader(const ModLoaderTypes loaders) -> int { + // https://docs.curseforge.com/?http#tocS_ModLoaderType + if (loaders & Forge) + return 1; + if (loaders & Fabric) + return 4; // TODO: remove this once Quilt drops official Fabric support - if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return Fabric; - return type; + if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* + return 4; // Quilt would probably be 5 + return 0; } }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ba0824cf..b99bfb26 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -6,9 +6,12 @@ #include "modplatform/flame/FlameAPI.h" #include "net/NetJob.h" +static ModPlatform::ProviderCapabilities ProviderCaps; + void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); + pack.provider = ModPlatform::Provider::FLAME; pack.name = Json::requireString(obj, "name"); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); @@ -25,6 +28,38 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } + + loadExtraPackData(pack, obj); +} + +void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + auto links_obj = Json::ensureObject(obj, "links"); + + pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + if(pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + if(pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + if(pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraDataLoaded = true; +} + +static QString enumToString(int hash_algorithm) +{ + switch(hash_algorithm){ + default: + case 1: + return "sha1"; + case 2: + return "md5"; + } } void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, @@ -38,28 +73,13 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj); + if(!file.addonId.isValid()) + file.addonId = pack.addonId; - auto versionArray = Json::requireArray(obj, "gameVersions"); - if (versionArray.isEmpty()) { - continue; - } - - ModPlatform::IndexedVersion file; - for (auto mcVer : versionArray) { - auto str = mcVer.toString(); - - if (str.contains('.')) - file.mcVersion.append(str); - } - - file.addonId = pack.addonId; - file.fileId = Json::requireInteger(obj, "id"); - file.date = Json::requireString(obj, "fileDate"); - file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); - file.fileName = Json::requireString(obj, "fileName"); - - unsortedVersions.append(file); + if(file.fileId.isValid()) // Heuristic to check if the returned value is valid + unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -70,3 +90,39 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion +{ + auto versionArray = Json::requireArray(obj, "gameVersions"); + if (versionArray.isEmpty()) { + return {}; + } + + ModPlatform::IndexedVersion file; + for (auto mcVer : versionArray) { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + } + + file.addonId = Json::requireInteger(obj, "modId"); + file.fileId = Json::requireInteger(obj, "id"); + file.date = Json::requireString(obj, "fileDate"); + file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::requireString(obj, "downloadUrl"); + file.fileName = Json::requireString(obj, "fileName"); + + auto hash_list = Json::ensureArray(obj, "hashes"); + for (auto h : hash_list) { + auto hash_entry = Json::ensureObject(h); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); + auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); + if (hash_types.contains(hash_algo)) { + file.hash = Json::requireString(hash_entry, "value"); + file.hash_type = hash_algo; + break; + } + } + return file; +} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index d3171d94..9c6c1c6c 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -12,9 +12,11 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance* inst); +auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index ac24c647..ad48b7b6 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -6,7 +6,6 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); auto logo = Json::requireObject(obj, "logo"); @@ -46,6 +45,32 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) if (!found) { throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); } + + loadIndexedInfo(pack, obj); +} + +void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj) +{ + auto links_obj = Json::ensureObject(obj, "links"); + + pack.extra.websiteUrl = Json::ensureString(links_obj, "websiteUrl"); + if(pack.extra.websiteUrl.endsWith('/')) + pack.extra.websiteUrl.chop(1); + + pack.extra.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + if(pack.extra.issuesUrl.endsWith('/')) + pack.extra.issuesUrl.chop(1); + + pack.extra.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + if(pack.extra.sourceUrl.endsWith('/')) + pack.extra.sourceUrl.chop(1); + + pack.extra.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + if(pack.extra.wikiUrl.endsWith('/')) + pack.extra.wikiUrl.chop(1); + + pack.extraInfoLoaded = true; + } void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) @@ -65,8 +90,12 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) // pick the latest version supported file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); - file.downloadUrl = Json::requireString(version, "downloadUrl"); - unsortedVersions.append(file); + file.downloadUrl = Json::ensureString(version, "downloadUrl"); + + // only add if we have a download URL (third party distribution is enabled) + if (!file.downloadUrl.isEmpty()) { + unsortedVersions.append(file); + } } auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; }; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 7ffa29c3..1ca0fc0e 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -20,6 +20,13 @@ struct IndexedVersion { QString downloadUrl; }; +struct ModpackExtra { + QString websiteUrl; + QString wikiUrl; + QString issuesUrl; + QString sourceUrl; +}; + struct IndexedPack { int addonId; @@ -28,13 +35,16 @@ struct IndexedPack QList<ModpackAuthor> authors; QString logoName; QString logoUrl; - QString websiteUrl; bool versionsLoaded = false; QVector<IndexedVersion> versions; + + bool extraInfoLoaded = false; + ModpackExtra extra; }; void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedInfo(IndexedPack&, QJsonObject&); void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); } diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index e4f90c1a..12a4b990 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest) auto obj = Json::requireObject(item); Flame::File file; loadFileV1(file, obj); - m.files.append(file); + m.files.insert(file.fileId,file); } m.overrides = Json::ensureString(manifest, "overrides", "overrides"); } @@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) loadManifestV1(m, obj); } -bool Flame::File::parseFromBytes(const QByteArray& bytes) +bool Flame::File::parseFromObject(const QJsonObject& obj) { - auto doc = Json::requireDocument(bytes); - if (!doc.isObject()) { - throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); - } - auto obj = Json::ensureObject(doc.object(), "data"); - fileName = Json::requireString(obj, "fileName"); - - QString rawUrl = Json::requireString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional type = File::Type::SingleFile; @@ -87,6 +75,25 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } + // get the hash + hash = QString(); + auto hashes = Json::ensureArray(obj, "hashes"); + for(QJsonValueRef item : hashes) { + auto hobj = Json::requireObject(item); + auto algo = Json::requireInteger(hobj, "algo"); + auto value = Json::requireString(hobj, "value"); + if (algo == 1) { + hash = value; + } + } + + + // may throw, if the project is blocked + QString rawUrl = Json::ensureString(obj, "downloadUrl"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if (!url.isValid()) { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } resolved = true; return true; diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 02f39f0e..677db1c3 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -1,26 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> #include <QVector> +#include <QMap> #include <QUrl> +#include <QJsonObject> namespace Flame { struct File { // NOTE: throws JSONValidationError - bool parseFromBytes(const QByteArray &bytes); + bool parseFromObject(const QJsonObject& object); int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional'. This is at the time of writing unused. bool required = true; + QString hash; + // NOTE: only set on blocked files ! Empty otherwise. + QString websiteUrl; // our bool resolved = false; QString fileName; QUrl url; - QString targetFolder = QLatin1Literal("mods"); + QString targetFolder = QStringLiteral("mods"); enum class Type { Unknown, @@ -54,7 +94,8 @@ struct Manifest QString name; QString version; QString author; - QVector<Flame::File> files; + //File id -> File + QMap<int,Flame::File> files; QString overrides; }; diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 6829b837..d7abd10f 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -31,6 +31,31 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const netJob->start(); } +void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) +{ + auto id_str = pack.addonId.toString(); + auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network()); + auto searchUrl = getModInfoURL(id_str); + + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->infoRequestFinished(doc, pack); + }); + + netJob->start(); +} + void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const { auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network()); diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 000620b2..87d77ad1 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -5,9 +5,11 @@ class NetworkModAPI : public ModAPI { public: void searchMods(CallerType* caller, SearchArgs&& args) const override; + void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; + virtual auto getModInfoURL(QString& id) const -> QString = 0; virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0; }; diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index 961fe868..4da6a866 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "PackFetchTask.h" #include "PrivatePackManager.h" @@ -103,7 +138,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) { - auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:3d!").arg(errorMsg, errorLine, errorCol); + auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); qWarning() << fullErrMsg; data.clear(); return false; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index c63a9f1e..83e14969 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "PackInstallTask.h" #include <QtConcurrent> @@ -88,7 +123,11 @@ void PackInstallTask::unzip() return; } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); +#else m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); +#endif connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp index 501e6003..1a81f026 100644 --- a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp +++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "PrivatePackManager.h" #include <QDebug> @@ -10,7 +45,13 @@ void PrivatePackManager::load() { try { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); + currentPacks = QSet<QString>(foo.begin(), foo.end()); +#else currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet(); +#endif + dirty = false; } catch(...) @@ -28,7 +69,7 @@ void PrivatePackManager::save() const } try { - QStringList list = currentPacks.toList(); + QStringList list = currentPacks.values(); FS::write(m_filename, list.join('\n').toUtf8()); dirty = false; } diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 33df6fa4..cac432cd 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FTBPackInstallTask.h" @@ -80,7 +99,7 @@ void PackInstallTask::onDownloadSucceeded() QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -197,10 +216,10 @@ void PackInstallTask::install() if(target.type != "modloader") continue; if(target.name == "forge") { - components->setComponentVersion("net.minecraftforge", target.version, true); + components->setComponentVersion("net.minecraftforge", target.version); } else if(target.name == "fabric") { - components->setComponentVersion("net.fabricmc.fabric-loader", target.version, true); + components->setComponentVersion("net.fabricmc.fabric-loader", target.version); } } @@ -220,6 +239,7 @@ void PackInstallTask::install() instance.setName(m_instName); instance.setIconKey(m_instIcon); + instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); instanceSettings->resumeSave(); emitSucceeded(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 6d642b5e..89e52d6c 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -20,6 +20,7 @@ #include "BuildConfig.h" #include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" #include <QDebug> @@ -28,30 +29,25 @@ class ModrinthAPI : public NetworkModAPI { public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; - static auto getModLoaderStrings(ModLoaderType type) -> const QStringList + static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList { QStringList l; - switch (type) + for (auto loader : {Forge, Fabric, Quilt}) { - case Unspecified: - for (auto loader : {Forge, Fabric, Quilt}) - { - l << ModAPI::getModLoaderString(loader); - } - break; - - case Quilt: - l << ModAPI::getModLoaderString(Fabric); - default: - l << ModAPI::getModLoaderString(type); + if ((types & loader) || types == Unspecified) + { + l << ModAPI::getModLoaderString(loader); + } } + if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there + l << ModAPI::getModLoaderString(Fabric); return l; } - static auto getModLoaderFilters(ModLoaderType type) -> const QString + static auto getModLoaderFilters(ModLoaderTypes types) -> const QString { QStringList l; - for (auto loader : getModLoaderStrings(type)) + for (auto loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } @@ -61,7 +57,7 @@ class ModrinthAPI : public NetworkModAPI { private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { - if (!validateModLoader(args.mod_loader)) { + if (!validateModLoaders(args.loaders)) { qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; return ""; } @@ -76,19 +72,24 @@ class ModrinthAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(args.sorting) - .arg(getModLoaderFilters(args.mod_loader)) + .arg(getModLoaderFilters(args.loaders)) .arg(getGameVersionsArray(args.versions)); }; + inline auto getModInfoURL(QString& id) const -> QString override + { + return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { return QString(BuildConfig.MODRINTH_PROD_URL + "/project/%1/version?" - "game_versions=[%2]" + "game_versions=[%2]&" "loaders=[\"%3\"]") - .arg(args.addonId) - .arg(getGameVersionsString(args.mcVersions)) - .arg(getModLoaderStrings(args.loader).join("\",\"")); + .arg(args.addonId, + getGameVersionsString(args.mcVersions), + getModLoaderStrings(args.loaders).join("\",\"")); }; auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString @@ -101,9 +102,9 @@ class ModrinthAPI : public NetworkModAPI { return s.isEmpty() ? QString() : QString("[%1],").arg(s); } - inline auto validateModLoader(ModLoaderType modLoader) const -> bool + inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { - return modLoader == Unspecified || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; + return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt)); } }; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index f7fa9864..b6f5490a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,19 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ #include "ModrinthPackIndex.h" #include "ModrinthAPI.h" @@ -24,10 +25,12 @@ #include "net/NetJob.h" static ModrinthAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); + pack.provider = ModPlatform::Provider::MODRINTH; pack.name = Json::requireString(obj, "title"); QString slug = Json::ensureString(obj, "slug", ""); @@ -45,6 +48,43 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) modAuthor.name = Json::requireString(obj, "author"); modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); + + // Modrinth can have more data than what's provided by the basic search :) + pack.extraDataLoaded = false; +} + +void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url"); + if(pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(obj, "source_url"); + if(pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url"); + if(pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraData.discordUrl = Json::ensureString(obj, "discord_url"); + if(pack.extraData.discordUrl.endsWith('/')) + pack.extraData.discordUrl.chop(1); + + auto donate_arr = Json::ensureArray(obj, "donation_urls"); + for(auto d : donate_arr){ + auto d_obj = Json::requireObject(d); + + ModPlatform::DonationData donate; + + donate.id = Json::ensureString(d_obj, "id"); + donate.platform = Json::ensureString(d_obj, "platform"); + donate.url = Json::ensureString(d_obj, "url"); + + pack.extraData.donate.append(donate); + } + + pack.extraDataLoaded = true; } void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, @@ -57,46 +97,10 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); - ModPlatform::IndexedVersion file; - file.addonId = Json::requireString(obj, "project_id"); - file.fileId = Json::requireString(obj, "id"); - file.date = Json::requireString(obj, "date_published"); - auto versionArray = Json::requireArray(obj, "game_versions"); - if (versionArray.empty()) { continue; } - for (auto mcVer : versionArray) { - file.mcVersion.append(mcVer.toString()); - } - auto loaders = Json::requireArray(obj, "loaders"); - for (auto loader : loaders) { - file.loaders.append(loader.toString()); - } - file.version = Json::requireString(obj, "name"); - - auto files = Json::requireArray(obj, "files"); - int i = 0; - - // Find correct file (needed in cases where one version may have multiple files) - // Will default to the last one if there's no primary (though I think Modrinth requires that - // at least one file is primary, idk) - // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed - while (i < files.count() - 1){ - auto parent = files[i].toObject(); - auto fileName = Json::requireString(parent, "filename"); - - // Grab the primary file, if available - if(Json::requireBoolean(parent, "primary")) - break; - - i++; - } - - auto parent = files[i].toObject(); - if (parent.contains("url")) { - file.downloadUrl = Json::requireString(parent, "url"); - file.fileName = Json::requireString(parent, "filename"); + auto file = loadIndexedPackVersion(obj); + if(file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); - } } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format @@ -106,3 +110,61 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion +{ + ModPlatform::IndexedVersion file; + + file.addonId = Json::requireString(obj, "project_id"); + file.fileId = Json::requireString(obj, "id"); + file.date = Json::requireString(obj, "date_published"); + auto versionArray = Json::requireArray(obj, "game_versions"); + if (versionArray.empty()) { + return {}; + } + for (auto mcVer : versionArray) { + file.mcVersion.append(mcVer.toString()); + } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { + file.loaders.append(loader.toString()); + } + file.version = Json::requireString(obj, "name"); + + auto files = Json::requireArray(obj, "files"); + int i = 0; + + // Find correct file (needed in cases where one version may have multiple files) + // Will default to the last one if there's no primary (though I think Modrinth requires that + // at least one file is primary, idk) + // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed + while (i < files.count() - 1) { + auto parent = files[i].toObject(); + auto fileName = Json::requireString(parent, "filename"); + + // Grab the primary file, if available + if (Json::requireBoolean(parent, "primary")) + break; + + i++; + } + + auto parent = files[i].toObject(); + if (parent.contains("url")) { + file.downloadUrl = Json::requireString(parent, "url"); + file.fileName = Json::requireString(parent, "filename"); + auto hash_list = Json::requireObject(parent, "hashes"); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); + for (auto& hash_type : hash_types) { + if (hash_list.contains(hash_type)) { + file.hash = Json::requireString(hash_list, hash_type); + file.hash_type = hash_type; + break; + } + } + + return file; + } + + return {}; +} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 7f306f25..b7936204 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -25,9 +25,11 @@ namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance* inst); +auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f1ad39ce..a4620df9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -42,6 +42,8 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include <QSet> + static ModrinthAPI api; namespace Modrinth { @@ -62,8 +64,35 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj) { pack.extra.body = Json::ensureString(obj, "body"); pack.extra.projectUrl = QString("https://modrinth.com/modpack/%1").arg(Json::ensureString(obj, "slug")); + + pack.extra.issuesUrl = Json::ensureString(obj, "issues_url"); + if(pack.extra.issuesUrl.endsWith('/')) + pack.extra.issuesUrl.chop(1); + pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); + if(pack.extra.sourceUrl.endsWith('/')) + pack.extra.sourceUrl.chop(1); + pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); + if(pack.extra.wikiUrl.endsWith('/')) + pack.extra.wikiUrl.chop(1); + + pack.extra.discordUrl = Json::ensureString(obj, "discord_url"); + if(pack.extra.discordUrl.endsWith('/')) + pack.extra.discordUrl.chop(1); + + auto donate_arr = Json::ensureArray(obj, "donation_urls"); + for(auto d : donate_arr){ + auto d_obj = Json::requireObject(d); + + DonationData donate; + + donate.id = Json::ensureString(d_obj, "id"); + donate.platform = Json::ensureString(d_obj, "platform"); + donate.url = Json::ensureString(d_obj, "url"); + + pack.extra.donate.append(donate); + } pack.extraInfoLoaded = true; } @@ -93,23 +122,6 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } -auto validateDownloadUrl(QUrl url) -> bool -{ - auto domain = url.host(); - if(domain == "cdn.modrinth.com") - return true; - if(domain == "edge.forgecdn.net") - return true; - if(domain == "media.forgecdn.net") - return true; - if(domain == "github.com") - return true; - if(domain == "raw.githubusercontent.com") - return true; - - return false; -} - auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion { ModpackVersion file; @@ -139,9 +151,6 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto url = Json::requireString(parent, "url"); - if(!validateDownloadUrl(url)) - continue; - file.download_url = url; if(is_primary) break; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index e5fc9a70..035dc62e 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -40,6 +40,7 @@ #include <QByteArray> #include <QCryptographicHash> +#include <QQueue> #include <QString> #include <QUrl> #include <QVector> @@ -48,22 +49,32 @@ class MinecraftInstance; namespace Modrinth { -struct File -{ +struct File { QString path; QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; - // TODO: should this support multiple download URLs, like the JSON does? - QUrl download; + QQueue<QUrl> downloads; +}; + +struct DonationData { + QString id; + QString platform; + QString url; }; struct ModpackExtra { QString body; QString projectUrl; + + QString issuesUrl; QString sourceUrl; QString wikiUrl; + QString discordUrl; + + QList<DonationData> donate; + }; struct ModpackVersion { diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp new file mode 100644 index 00000000..0782b9f4 --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "Packwiz.h" + +#include <QDebug> +#include <QDir> +#include <QObject> + +#include "toml.h" +#include "FileSystem.h" + +#include "minecraft/mod/Mod.h" +#include "modplatform/ModIndex.h" + +namespace Packwiz { + +auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString +{ + QFile index_file(index_dir.absoluteFilePath(normalized_fname)); + + QString real_fname = normalized_fname; + if (!index_file.exists()) { + // Tries to get similar entries + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) { + real_fname = file_name; + break; + } + } + + if(should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)){ + qCritical() << "Could not find a match for a valid metadata file!"; + qCritical() << "File: " << normalized_fname; + return {}; + } + } + + return real_fname; +} + +// Helpers +static inline auto indexFileName(QString const& mod_name) -> QString +{ + if(mod_name.endsWith(".pw.toml")) + return mod_name; + return QString("%1.pw.toml").arg(mod_name); +} + +static ModPlatform::ProviderCapabilities ProviderCaps; + +// Helper functions for extracting data from the TOML file +auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString +{ + toml_datum_t var = toml_string_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + QString tmp = var.u.s; + free(var.u.s); + + return tmp; +} + +auto intEntry(toml_table_t* parent, const char* entry_name) -> int +{ + toml_datum_t var = toml_int_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + return var.u.i; +} + + +auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod +{ + Mod mod; + + mod.name = mod_pack.name; + mod.filename = mod_version.fileName; + + if(mod_pack.provider == ModPlatform::Provider::FLAME){ + mod.mode = "metadata:curseforge"; + } + else { + mod.mode = "url"; + mod.url = mod_version.downloadUrl; + } + + mod.hash_format = mod_version.hash_type; + mod.hash = mod_version.hash; + + mod.provider = mod_pack.provider; + mod.file_id = mod_version.fileId; + mod.project_id = mod_pack.addonId; + + return mod; +} + +auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod +{ + auto mod_name = internal_mod.name(); + + // Try getting metadata if it exists + Mod mod { getIndexForMod(index_dir, mod_name) }; + if(mod.isValid()) + return mod; + + qWarning() << QString("Tried to create mod metadata with a Mod without metadata!"); + + return {}; +} + +void V1::updateModIndex(QDir& index_dir, Mod& mod) +{ + if(!mod.isValid()){ + qCritical() << QString("Tried to update metadata of an invalid mod!"); + return; + } + + // Ensure the corresponding mod's info exists, and create it if not + + auto normalized_fname = indexFileName(mod.name); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + // There's already data on there! + // TODO: We should do more stuff here, as the user is likely trying to + // override a file. In this case, check versions and ask the user what + // they want to do! + if (index_file.exists()) { index_file.remove(); } + + if (!index_file.open(QIODevice::ReadWrite)) { + qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); + return; + } + + // Put TOML data into the file + QTextStream in_stream(&index_file); + auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); }; + + { + addToStream("name", mod.name); + addToStream("filename", mod.filename); + addToStream("side", mod.side); + + in_stream << QString("\n[download]\n"); + addToStream("mode", mod.mode); + addToStream("url", mod.url.toString()); + addToStream("hash-format", mod.hash_format); + addToStream("hash", mod.hash); + + in_stream << QString("\n[update]\n"); + in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); + switch(mod.provider){ + case(ModPlatform::Provider::FLAME): + in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); + in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); + break; + case(ModPlatform::Provider::MODRINTH): + addToStream("mod-id", mod.mod_id().toString()); + addToStream("version", mod.version().toString()); + break; + } + } + + index_file.close(); +} + +void V1::deleteModIndex(QDir& index_dir, QString& mod_name) +{ + auto normalized_fname = indexFileName(mod_name); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + if (real_fname.isEmpty()) + return; + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + if(!index_file.exists()){ + qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); + return; + } + + if(!index_file.remove()){ + qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); + } +} + +auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod +{ + Mod mod; + + auto normalized_fname = indexFileName(index_file_name); + auto real_fname = getRealIndexName(index_dir, normalized_fname, true); + if (real_fname.isEmpty()) + return {}; + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + if (!index_file.open(QIODevice::ReadOnly)) { + qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name); + return {}; + } + + toml_table_t* table = nullptr; + + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + char errbuf[200]; + auto file_bytearray = index_file.readAll(); + table = toml_parse(file_bytearray.data(), errbuf, sizeof(errbuf)); + + index_file.close(); + + if (!table) { + qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name)); + qWarning() << "Reason: " << QString(errbuf); + return {}; + } + + { // Basic info + mod.name = stringEntry(table, "name"); + mod.filename = stringEntry(table, "filename"); + mod.side = stringEntry(table, "side"); + } + + { // [download] info + toml_table_t* download_table = toml_table_in(table, "download"); + if (!download_table) { + qCritical() << QString("No [download] section found on mod metadata!"); + return {}; + } + + mod.mode = stringEntry(download_table, "mode"); + mod.url = stringEntry(download_table, "url"); + mod.hash_format = stringEntry(download_table, "hash-format"); + mod.hash = stringEntry(download_table, "hash"); + } + + { // [update] info + using Provider = ModPlatform::Provider; + + toml_table_t* update_table = toml_table_in(table, "update"); + if (!update_table) { + qCritical() << QString("No [update] section found on mod metadata!"); + return {}; + } + + toml_table_t* mod_provider_table = nullptr; + if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::FLAME)))) { + mod.provider = Provider::FLAME; + mod.file_id = intEntry(mod_provider_table, "file-id"); + mod.project_id = intEntry(mod_provider_table, "project-id"); + } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::MODRINTH)))) { + mod.provider = Provider::MODRINTH; + mod.mod_id() = stringEntry(mod_provider_table, "mod-id"); + mod.version() = stringEntry(mod_provider_table, "version"); + } else { + qCritical() << QString("No mod provider on mod metadata!"); + return {}; + } + + } + + toml_free(table); + + return mod; +} + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h new file mode 100644 index 00000000..3c99769c --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include "modplatform/ModIndex.h" + +#include <QString> +#include <QUrl> +#include <QVariant> + +struct toml_table_t; +class QDir; + +// Mod from launcher/minecraft/mod/Mod.h +class Mod; + +namespace Packwiz { + +auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; + +auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString; +auto intEntry(toml_table_t* parent, const char* entry_name) -> int; + +class V1 { + public: + struct Mod { + QString name {}; + QString filename {}; + // FIXME: make side an enum + QString side {"both"}; + + // [download] + QString mode {}; + QUrl url {}; + QString hash_format {}; + QString hash {}; + + // [update] + ModPlatform::Provider provider {}; + QVariant file_id {}; + QVariant project_id {}; + + public: + // This is a totally heuristic, but should work for now. + auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); } + + // Different providers can use different names for the same thing + // Modrinth-specific + auto mod_id() -> QVariant& { return project_id; } + auto version() -> QVariant& { return file_id; } + }; + + /* Generates the object representing the information in a mod.pw.toml file via + * its common representation in the launcher, when downloading mods. + * */ + static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; + /* Generates the object representing the information in a mod.pw.toml file via + * its common representation in the launcher. + * */ + static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod; + + /* Updates the mod index for the provided mod. + * This creates a new index if one does not exist already + * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one. + * */ + static void updateModIndex(QDir& index_dir, Mod& mod); + + /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QString& mod_name); + + /* Gets the metadata for a mod with a particular name. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod; +}; + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp new file mode 100644 index 00000000..d6251148 --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <QTemporaryDir> +#include <QTest> + +#include "Packwiz.h" + +class PackwizTest : public QObject { + Q_OBJECT + + private slots: + // Files taken from https://github.com/packwiz/packwiz-example-pack + void loadFromFile_Modrinth() + { + QString source = QFINDTESTDATA("testdata"); + + QDir index_dir(source); + QString name_mod("borderless-mining.pw.toml"); + QVERIFY(index_dir.entryList().contains(name_mod)); + + auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + + QVERIFY(metadata.isValid()); + + QCOMPARE(metadata.name, "Borderless Mining"); + QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); + QCOMPARE(metadata.side, "client"); + + QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); + QCOMPARE(metadata.hash_format, "sha512"); + QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"); + + QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH); + QCOMPARE(metadata.version(), "ug2qKTPR"); + QCOMPARE(metadata.mod_id(), "kYq5qkSL"); + } + + void loadFromFile_Curseforge() + { + QString source = QFINDTESTDATA("testdata"); + + QDir index_dir(source); + QString name_mod("screenshot-to-clipboard-fabric.pw.toml"); + QVERIFY(index_dir.entryList().contains(name_mod)); + + // Try without the .pw.toml at the end + name_mod.chop(8); + + auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + + QVERIFY(metadata.isValid()); + + QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); + QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); + QCOMPARE(metadata.side, "both"); + + QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); + QCOMPARE(metadata.hash_format, "murmur2"); + QCOMPARE(metadata.hash, "1781245820"); + + QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME); + QCOMPARE(metadata.file_id, 3509043); + QCOMPARE(metadata.project_id, 327154); + } +}; + +QTEST_GUILESS_MAIN(PackwizTest) + +#include "Packwiz_test.moc" diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml new file mode 100644 index 00000000..16545fd4 --- /dev/null +++ b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml @@ -0,0 +1,13 @@ +name = "Borderless Mining" +filename = "borderless-mining-1.1.1+1.18.jar" +side = "client" + +[download] +url = "https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar" +hash-format = "sha512" +hash = "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d" + +[update] +[update.modrinth] +mod-id = "kYq5qkSL" +version = "ug2qKTPR" diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml new file mode 100644 index 00000000..87d70ada --- /dev/null +++ b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml @@ -0,0 +1,13 @@ +name = "Screenshot to Clipboard (Fabric)" +filename = "screenshot-to-clipboard-1.0.7-fabric.jar" +side = "both" + +[download] +url = "https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar" +hash-format = "murmur2" +hash = "1781245820" + +[update] +[update.curseforge] +file-id = 3509043 +project-id = 327154 diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp index 16fe0b0e..e52a7ec0 100644 --- a/launcher/modplatform/technic/SolderPackManifest.cpp +++ b/launcher/modplatform/technic/SolderPackManifest.cpp @@ -37,7 +37,7 @@ void loadPack(Pack& v, QJsonObject& obj) static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) { b.name = Json::requireString(obj, "name"); - b.version = Json::requireString(obj, "version"); + b.version = Json::ensureString(obj, "version", ""); b.md5 = Json::requireString(obj, "md5"); b.url = Json::requireString(obj, "url"); } diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 782fb9b2..95feb4b2 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -185,13 +185,22 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); } } - else if (libraryName.startsWith("net.minecraftforge:minecraftforge:")) + else { - components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2)); - } - else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) - { - components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2)); + // <Technic library name prefix> -> <our component name> + static QMap<QString, QString> loaderMap { + {"net.minecraftforge:minecraftforge:", "net.minecraftforge"}, + {"net.fabricmc:fabric-loader:", "net.fabricmc.fabric-loader"}, + {"org.quiltmc:quilt-loader:", "org.quiltmc.quilt-loader"} + }; + for (const auto& loader : loaderMap.keys()) + { + if (libraryName.startsWith(loader)) + { + components->setComponentVersion(loaderMap.value(loader), libraryName.section(':', 2)); + break; + } + } } } } diff --git a/launcher/mojang/PackageManifest_test.cpp b/launcher/mojang/PackageManifest_test.cpp index d4c55c5a..e8da4266 100644 --- a/launcher/mojang/PackageManifest_test.cpp +++ b/launcher/mojang/PackageManifest_test.cpp @@ -1,6 +1,5 @@ #include <QTest> #include <QDebug> -#include "TestUtil.h" #include "mojang/PackageManifest.h" diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 20e6764c..501318a1 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -1,62 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "Sink.h" namespace Net { + /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. + * FIXME: It is possible that the QByteArray is freed while we're doing some operation on it, + * causing a segmentation fault. */ -class ByteArraySink : public Sink -{ -public: - ByteArraySink(QByteArray *output) - :m_output(output) - { - // nil - }; +class ByteArraySink : public Sink { + public: + ByteArraySink(QByteArray* output) : m_output(output){}; - virtual ~ByteArraySink() - { - // nil - } + virtual ~ByteArraySink() = default; -public: - JobStatus init(QNetworkRequest & request) override + public: + auto init(QNetworkRequest& request) -> Task::State override { m_output->clear(); - if(initAllValidators(request)) - return Job_InProgress; - return Job_Failed; + if (initAllValidators(request)) + return Task::State::Running; + return Task::State::Failed; }; - JobStatus write(QByteArray & data) override + auto write(QByteArray& data) -> Task::State override { m_output->append(data); - if(writeAllValidators(data)) - return Job_InProgress; - return Job_Failed; + if (writeAllValidators(data)) + return Task::State::Running; + return Task::State::Failed; } - JobStatus abort() override + auto abort() -> Task::State override { m_output->clear(); failAllValidators(); - return Job_Failed; + return Task::State::Failed; } - JobStatus finalize(QNetworkReply &reply) override + auto finalize(QNetworkReply& reply) -> Task::State override { - if(finalizeAllValidators(reply)) - return Job_Finished; - return Job_Failed; + if (finalizeAllValidators(reply)) + return Task::State::Succeeded; + return Task::State::Failed; } - bool hasLocalData() override - { - return false; - } + auto hasLocalData() -> bool override { return false; } -private: - QByteArray * m_output; + private: + QByteArray* m_output; }; -} +} // namespace Net diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 0d6b19c2..a2ca2c7a 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -1,55 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "Validator.h" + #include <QCryptographicHash> -#include <memory> #include <QFile> namespace Net { -class ChecksumValidator: public Validator -{ -public: /* con/des */ +class ChecksumValidator : public Validator { + public: ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) - :m_checksum(algorithm), m_expected(expected) - { - }; - virtual ~ChecksumValidator() {}; + : m_checksum(algorithm), m_expected(expected){}; + virtual ~ChecksumValidator() = default; -public: /* methods */ - bool init(QNetworkRequest &) override + public: + auto init(QNetworkRequest&) -> bool override { m_checksum.reset(); return true; } - bool write(QByteArray & data) override + + auto write(QByteArray& data) -> bool override { m_checksum.addData(data); return true; } - bool abort() override - { - return true; - } - bool validate(QNetworkReply &) override + + auto abort() -> bool override { return true; } + + auto validate(QNetworkReply&) -> bool override { - if(m_expected.size() && m_expected != hash()) - { + if (m_expected.size() && m_expected != hash()) { qWarning() << "Checksum mismatch, download is bad."; return false; } return true; } - QByteArray hash() - { - return m_checksum.result(); - } - void setExpected(QByteArray expected) - { - m_expected = expected; - } -private: /* data */ + auto hash() -> QByteArray { return m_checksum.result(); } + + void setExpected(QByteArray expected) { m_expected = expected; } + + private: QCryptographicHash m_checksum; QByteArray m_expected; }; -}
\ No newline at end of file +} // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 65cc8f67..3061e32e 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -1,22 +1,42 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "Download.h" #include <QDateTime> -#include <QDebug> #include <QFileInfo> #include "ByteArraySink.h" @@ -25,38 +45,38 @@ #include "MetaCacheSink.h" #include "BuildConfig.h" +#include "Application.h" namespace Net { Download::Download() : NetAction() { - m_status = Job_NotStarted; + m_state = State::Inactive; } -Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) +auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); auto cachedNode = new MetaCacheSink(entry, md5Node); dl->m_sink.reset(cachedNode); - dl->m_target_path = entry->getFullPath(); return dl; } -Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options) +auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); return dl; } -Download::Ptr Download::makeFile(QUrl url, QString path, Options options) +auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new FileSink(path)); @@ -68,69 +88,74 @@ void Download::addValidator(Validator* v) m_sink->addValidator(v); } -void Download::startImpl() +void Download::executeTask() { - if (m_status == Job_Aborted) { + setStatus(tr("Downloading %1").arg(m_url.toString())); + + if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); - emit aborted(m_index_within_job); + emitAborted(); return; } + QNetworkRequest request(m_url); - m_status = m_sink->init(request); - switch (m_status) { - case Job_Finished: - emit succeeded(m_index_within_job); + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: + emit succeeded(); qDebug() << "Download cache hit " << m_url.toString(); return; - case Job_InProgress: + case State::Running: qDebug() << "Downloading " << m_url.toString(); break; - case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. - case Job_NotStarted: - case Job_Failed: - emit failed(m_index_within_job); + case State::Inactive: + case State::Failed: + emitFailed(); return; - case Job_Aborted: + case State::AbortedByUser: + emitAborted(); return; } - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (request.url().host().contains("api.curseforge.com")) { - request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8()); + request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); }; QNetworkReply* rep = m_network->get(request); m_reply.reset(rep); - connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); - connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, &QNetworkReply::downloadProgress, this, &Download::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &Download::downloadFinished); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +#else connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +#endif connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead); } void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + setProgress(bytesReceived, bytesTotal); } void Download::downloadError(QNetworkReply::NetworkError error) { if (error == QNetworkReply::OperationCanceledError) { qCritical() << "Aborted " << m_url.toString(); - m_status = Job_Aborted; + m_state = State::AbortedByUser; } else { if (m_options & Option::AcceptLocalFiles) { if (m_sink->hasLocalData()) { - m_status = Job_Failed_Proceed; + m_state = State::Succeeded; return; } } // error happened during download. qCritical() << "Failed " << m_url.toString() << " with reason " << error; - m_status = Job_Failed; + m_state = State::Failed; } } @@ -145,7 +170,7 @@ void Download::sslErrors(const QList<QSslError>& errors) } } -bool Download::handleRedirect() +auto Download::handleRedirect() -> bool { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); if (!redirect.isValid()) { @@ -194,7 +219,8 @@ bool Download::handleRedirect() m_url = QUrl(redirect.toString()); qDebug() << "Following redirect to " << m_url.toString(); - start(m_network); + startAction(m_network); + return true; } @@ -207,74 +233,71 @@ void Download::downloadFinished() } // if the download failed before this point ... - if (m_status == Job_Failed_Proceed) { + if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) + { qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit succeeded(m_index_within_job); + emit succeeded(); return; - } else if (m_status == Job_Failed) { + } else if (m_state == State::Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit failed(m_index_within_job); + emit failed(""); return; - } else if (m_status == Job_Aborted) { + } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit aborted(m_index_within_job); + emit aborted(); return; } // make sure we got all the remaining data, if any auto data = m_reply->readAll(); if (data.size()) { - qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; - m_status = m_sink->write(data); + qDebug() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); } // otherwise, finalize the whole graph - m_status = m_sink->finalize(*m_reply.get()); - if (m_status != Job_Finished) { + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit failed(m_index_within_job); + emit failed(""); return; } + m_reply.reset(); qDebug() << "Download succeeded:" << m_url.toString(); - emit succeeded(m_index_within_job); + emit succeeded(); } void Download::downloadReadyRead() { - if (m_status == Job_InProgress) { + if (m_state == State::Running) { auto data = m_reply->readAll(); - m_status = m_sink->write(data); - if (m_status == Job_Failed) { - qCritical() << "Failed to process response chunk for " << m_target_path; + m_state = m_sink->write(data); + if (m_state == State::Failed) { + qCritical() << "Failed to process response chunk"; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; } else { - qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; + qCritical() << "Cannot write download data! illegal status " << m_status; } } } // namespace Net -bool Net::Download::abort() +auto Net::Download::abort() -> bool { if (m_reply) { m_reply->abort(); } else { - m_status = Job_Aborted; + m_state = State::AbortedByUser; } return true; } - -bool Net::Download::canAbort() -{ - return true; -} diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 0f9bfe7f..20932944 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -1,77 +1,88 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include "NetAction.h" #include "HttpMetaCache.h" -#include "Validator.h" +#include "NetAction.h" #include "Sink.h" +#include "Validator.h" #include "QObjectPtr.h" namespace Net { -class Download : public NetAction -{ +class Download : public NetAction { Q_OBJECT -public: /* types */ - typedef shared_qobject_ptr<class Download> Ptr; - enum class Option - { - NoOptions = 0, - AcceptLocalFiles = 1 - }; + public: + using Ptr = shared_qobject_ptr<class Download>; + enum class Option { NoOptions = 0, AcceptLocalFiles = 1 }; Q_DECLARE_FLAGS(Options, Option) -protected: /* con/des */ + protected: explicit Download(); -public: - virtual ~Download(){}; - static Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions); - static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions); - static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions); -public: /* methods */ - QString getTargetFilepath() - { - return m_target_path; - } - void addValidator(Validator * v); - bool abort() override; - bool canAbort() override; + public: + ~Download() override = default; + + static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; + + public: + void addValidator(Validator* v); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; -private: /* methods */ - bool handleRedirect(); + private: + auto handleRedirect() -> bool; -protected slots: + protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList<QSslError> & errors); + void sslErrors(const QList<QSslError>& errors); void downloadFinished() override; void downloadReadyRead() override; -public slots: - void startImpl() override; + public slots: + void executeTask() override; -private: /* data */ - // FIXME: remove this, it has no business being here. - QString m_target_path; + private: std::unique_ptr<Sink> m_sink; Options m_options; }; -} +} // namespace Net Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options) diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 7e9b8929..ba0caf6c 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,109 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "FileSink.h" -#include <QFile> -#include <QFileInfo> + #include "FileSystem.h" namespace Net { -FileSink::FileSink(QString filename) - :m_filename(filename) -{ - // nil -} - -FileSink::~FileSink() -{ - // nil -} - -JobStatus FileSink::init(QNetworkRequest& request) +Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); - if(result != Job_InProgress) - { + if (result != Task::State::Running) { return result; } + // create a new save file and open it for writing - if (!FS::ensureFilePathExists(m_filename)) - { + if (!FS::ensureFilePathExists(m_filename)) { qCritical() << "Could not create folder for " + m_filename; - return Job_Failed; + return Task::State::Failed; } + wroteAnyData = false; m_output_file.reset(new QSaveFile(m_filename)); - if (!m_output_file->open(QIODevice::WriteOnly)) - { + if (!m_output_file->open(QIODevice::WriteOnly)) { qCritical() << "Could not open " + m_filename + " for writing"; - return Job_Failed; + return Task::State::Failed; } - if(initAllValidators(request)) - return Job_InProgress; - return Job_Failed; + if (initAllValidators(request)) + return Task::State::Running; + return Task::State::Failed; } -JobStatus FileSink::initCache(QNetworkRequest &) +Task::State FileSink::write(QByteArray& data) { - return Job_InProgress; -} - -JobStatus FileSink::write(QByteArray& data) -{ - if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) - { + if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { qCritical() << "Failed writing into " + m_filename; m_output_file->cancelWriting(); m_output_file.reset(); wroteAnyData = false; - return Job_Failed; + return Task::State::Failed; } + wroteAnyData = true; - return Job_InProgress; + return Task::State::Running; } -JobStatus FileSink::abort() +Task::State FileSink::abort() { m_output_file->cancelWriting(); failAllValidators(); - return Job_Failed; + return Task::State::Failed; } -JobStatus FileSink::finalize(QNetworkReply& reply) +Task::State FileSink::finalize(QNetworkReply& reply) { bool gotFile = false; QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); bool validStatus = false; int statusCode = statusCodeV.toInt(&validStatus); - if(validStatus) - { + if (validStatus) { // this leaves out 304 Not Modified gotFile = statusCode == 200 || statusCode == 203; } + // if we wrote any data to the save file, we try to commit the data to the real file. // if it actually got a proper file, we write it even if it was empty - if (gotFile || wroteAnyData) - { + if (gotFile || wroteAnyData) { // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits - if(!finalizeAllValidators(reply)) - return Job_Failed; + if (!finalizeAllValidators(reply)) + return Task::State::Failed; + // nothing went wrong... - if (!m_output_file->commit()) - { + if (!m_output_file->commit()) { qCritical() << "Failed to commit changes to " << m_filename; m_output_file->cancelWriting(); - return Job_Failed; + return Task::State::Failed; } } + // then get rid of the save file m_output_file.reset(); return finalizeCache(reply); } -JobStatus FileSink::finalizeCache(QNetworkReply &) +Task::State FileSink::initCache(QNetworkRequest&) { - return Job_Finished; + return Task::State::Running; +} + +Task::State FileSink::finalizeCache(QNetworkReply&) +{ + return Task::State::Succeeded; } bool FileSink::hasLocalData() @@ -111,4 +133,4 @@ bool FileSink::hasLocalData() QFileInfo info(m_filename); return info.exists() && info.size() != 0; } -} +} // namespace Net diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 875fe511..dffbdca6 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -1,28 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once -#include "Sink.h" + #include <QSaveFile> +#include "Sink.h" + namespace Net { -class FileSink : public Sink -{ -public: /* con/des */ - FileSink(QString filename); - virtual ~FileSink(); - -public: /* methods */ - JobStatus init(QNetworkRequest & request) override; - JobStatus write(QByteArray & data) override; - JobStatus abort() override; - JobStatus finalize(QNetworkReply & reply) override; - bool hasLocalData() override; - -protected: /* methods */ - virtual JobStatus initCache(QNetworkRequest &); - virtual JobStatus finalizeCache(QNetworkReply &reply); - -protected: /* data */ +class FileSink : public Sink { + public: + FileSink(QString filename) : m_filename(filename){}; + virtual ~FileSink() = default; + + public: + auto init(QNetworkRequest& request) -> Task::State override; + auto write(QByteArray& data) -> Task::State override; + auto abort() -> Task::State override; + auto finalize(QNetworkReply& reply) -> Task::State override; + + auto hasLocalData() -> bool override; + + protected: + virtual auto initCache(QNetworkRequest&) -> Task::State; + virtual auto finalizeCache(QNetworkReply& reply) -> Task::State; + + protected: QString m_filename; bool wroteAnyData = false; std::unique_ptr<QSaveFile> m_output_file; }; -} +} // namespace Net diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 8734e0bf..4d86c0b8 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -1,43 +1,60 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "HttpMetaCache.h" #include "FileSystem.h" +#include "Json.h" -#include <QFileInfo> -#include <QFile> -#include <QDateTime> #include <QCryptographicHash> +#include <QDateTime> +#include <QFile> +#include <QFileInfo> #include <QDebug> -#include <QJsonDocument> -#include <QJsonArray> -#include <QJsonObject> - -QString MetaEntry::getFullPath() +auto MetaEntry::getFullPath() -> QString { // FIXME: make local? return FS::PathCombine(basePath, relativePath); } -HttpMetaCache::HttpMetaCache(QString path) : QObject() +HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) { - m_index_file = path; saveBatchingTimer.setSingleShot(true); saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); + connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow())); } @@ -47,45 +64,42 @@ HttpMetaCache::~HttpMetaCache() SaveNow(); } -MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path) +auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr { // no base. no base path. can't store - if (!m_entries.contains(base)) - { + if (!m_entries.contains(base)) { // TODO: log problem - return MetaEntryPtr(); + return {}; } - EntryMap &map = m_entries[base]; - if (map.entry_list.contains(resource_path)) - { + + EntryMap& map = m_entries[base]; + if (map.entry_list.contains(resource_path)) { return map.entry_list[resource_path]; } - return MetaEntryPtr(); + + return {}; } -MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) +auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { auto entry = getEntry(base, resource_path); // it's not present? generate a default stale entry - if (!entry) - { + if (!entry) { return staleEntry(base, resource_path); } - auto &selected_base = m_entries[base]; + auto& selected_base = m_entries[base]; QString real_path = FS::PathCombine(selected_base.base_path, resource_path); QFileInfo finfo(real_path); // is the file really there? if not -> stale - if (!finfo.isFile() || !finfo.isReadable()) - { + if (!finfo.isFile() || !finfo.isReadable()) { // if the file doesn't exist, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } - if (!expected_etag.isEmpty() && expected_etag != entry->etag) - { + if (!expected_etag.isEmpty() && expected_etag != entry->etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); @@ -93,18 +107,15 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); - if (file_last_changed != entry->local_changed_timestamp) - { + if (file_last_changed != entry->local_changed_timestamp) { QFile input(real_path); input.open(QIODevice::ReadOnly); - QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5) - .toHex() - .constData(); - if (entry->md5sum != md5sum) - { + QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); + if (entry->md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } + // md5sums matched... keep entry and save the new state to file entry->local_changed_timestamp = file_last_changed; SaveEventually(); @@ -115,42 +126,42 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS return entry; } -bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) +auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { - if (!m_entries.contains(stale_entry->baseId)) - { - qCritical() << "Cannot add entry with unknown base: " - << stale_entry->baseId.toLocal8Bit(); + if (!m_entries.contains(stale_entry->baseId)) { + qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit(); return false; } - if (stale_entry->stale) - { + + if (stale_entry->stale) { qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); return false; } + m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; SaveEventually(); + return true; } -bool HttpMetaCache::evictEntry(MetaEntryPtr entry) +auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool { - if(entry) - { - entry->stale = true; - SaveEventually(); - return true; - } - return false; + if (!entry) + return false; + + entry->stale = true; + SaveEventually(); + return true; } -MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path) +auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); foo->baseId = base; foo->basePath = getBasePath(base); foo->relativePath = resource_path; foo->stale = true; + return MetaEntryPtr(foo); } @@ -159,24 +170,25 @@ void HttpMetaCache::addBase(QString base, QString base_root) // TODO: report error if (m_entries.contains(base)) return; + // TODO: check if the base path is valid EntryMap foo; foo.base_path = base_root; m_entries[base] = foo; } -QString HttpMetaCache::getBasePath(QString base) +auto HttpMetaCache::getBasePath(QString base) -> QString { - if (m_entries.contains(base)) - { + if (m_entries.contains(base)) { return m_entries[base].base_path; } - return QString(); + + return {}; } void HttpMetaCache::Load() { - if(m_index_file.isNull()) + if (m_index_file.isNull()) return; QFile index(m_index_file); @@ -184,41 +196,35 @@ void HttpMetaCache::Load() return; QJsonDocument json = QJsonDocument::fromJson(index.readAll()); - if (!json.isObject()) - return; - auto root = json.object(); + + auto root = Json::requireObject(json, "HttpMetaCache root"); + // check file version first - auto version_val = root.value("version"); - if (!version_val.isString()) - return; - if (version_val.toString() != "1") + auto version_val = Json::ensureString(root, "version"); + if (version_val != "1") return; // read the entry array - auto entries_val = root.value("entries"); - if (!entries_val.isArray()) - return; - QJsonArray array = entries_val.toArray(); - for (auto element : array) - { - if (!element.isObject()) - return; - auto element_obj = element.toObject(); - QString base = element_obj.value("base").toString(); + auto array = Json::ensureArray(root, "entries"); + for (auto element : array) { + auto element_obj = Json::ensureObject(element); + auto base = Json::ensureString(element_obj, "base"); if (!m_entries.contains(base)) continue; - auto &entrymap = m_entries[base]; + + auto& entrymap = m_entries[base]; + auto foo = new MetaEntry(); foo->baseId = base; - QString path = foo->relativePath = element_obj.value("path").toString(); - foo->md5sum = element_obj.value("md5sum").toString(); - foo->etag = element_obj.value("etag").toString(); - foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); - foo->remote_changed_timestamp = - element_obj.value("remote_changed_timestamp").toString(); + foo->relativePath = Json::ensureString(element_obj, "path"); + foo->md5sum = Json::ensureString(element_obj, "md5sum"); + foo->etag = Json::ensureString(element_obj, "etag"); + foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); + foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); // presumed innocent until closer examination foo->stale = false; - entrymap.entry_list[path] = MetaEntryPtr(foo); + + entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo); } } @@ -231,42 +237,36 @@ void HttpMetaCache::SaveEventually() void HttpMetaCache::SaveNow() { - if(m_index_file.isNull()) + if (m_index_file.isNull()) return; + QJsonObject toplevel; - toplevel.insert("version", QJsonValue(QString("1"))); + Json::writeString(toplevel, "version", "1"); + QJsonArray entriesArr; - for (auto group : m_entries) - { - for (auto entry : group.entry_list) - { + for (auto group : m_entries) { + for (auto entry : group.entry_list) { // do not save stale entries. they are dead. - if(entry->stale) - { + if (entry->stale) { continue; } + QJsonObject entryObj; - entryObj.insert("base", QJsonValue(entry->baseId)); - entryObj.insert("path", QJsonValue(entry->relativePath)); - entryObj.insert("md5sum", QJsonValue(entry->md5sum)); - entryObj.insert("etag", QJsonValue(entry->etag)); - entryObj.insert("last_changed_timestamp", - QJsonValue(double(entry->local_changed_timestamp))); + Json::writeString(entryObj, "base", entry->baseId); + Json::writeString(entryObj, "path", entry->relativePath); + Json::writeString(entryObj, "md5sum", entry->md5sum); + Json::writeString(entryObj, "etag", entry->etag); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); if (!entry->remote_changed_timestamp.isEmpty()) - entryObj.insert("remote_changed_timestamp", - QJsonValue(entry->remote_changed_timestamp)); + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); entriesArr.append(entryObj); } } toplevel.insert("entries", entriesArr); - QJsonDocument doc(toplevel); - try - { - FS::write(m_index_file, doc.toJson()); - } - catch (const Exception &e) - { + try { + Json::write(toplevel, m_index_file); + } catch (const Exception& e) { qWarning() << e.what(); } } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 1c10e8c7..e944b3d5 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -1,122 +1,122 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include <QString> + #include <QMap> -#include <qtimer.h> +#include <QString> +#include <QTimer> #include <memory> class HttpMetaCache; -class MetaEntry -{ -friend class HttpMetaCache; -protected: - MetaEntry() {} -public: - bool isStale() - { - return stale; - } - void setStale(bool stale) - { - this->stale = stale; - } - QString getFullPath(); - QString getRemoteChangedTimestamp() - { - return remote_changed_timestamp; - } - void setRemoteChangedTimestamp(QString remote_changed_timestamp) - { - this->remote_changed_timestamp = remote_changed_timestamp; - } - void setLocalChangedTimestamp(qint64 timestamp) - { - local_changed_timestamp = timestamp; - } - QString getETag() - { - return etag; - } - void setETag(QString etag) - { - this->etag = etag; - } - QString getMD5Sum() - { - return md5sum; - } - void setMD5Sum(QString md5sum) - { - this->md5sum = md5sum; - } -protected: +class MetaEntry { + friend class HttpMetaCache; + + protected: + MetaEntry() = default; + + public: + auto isStale() -> bool { return stale; } + void setStale(bool stale) { this->stale = stale; } + + auto getFullPath() -> QString; + + auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; } + void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; } + + auto getETag() -> QString { return etag; } + void setETag(QString etag) { this->etag = etag; } + + auto getMD5Sum() -> QString { return md5sum; } + void setMD5Sum(QString md5sum) { this->md5sum = md5sum; } + + protected: QString baseId; QString basePath; QString relativePath; QString md5sum; QString etag; qint64 local_changed_timestamp = 0; - QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time + QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time bool stale = true; }; -typedef std::shared_ptr<MetaEntry> MetaEntryPtr; +using MetaEntryPtr = std::shared_ptr<MetaEntry>; -class HttpMetaCache : public QObject -{ +class HttpMetaCache : public QObject { Q_OBJECT -public: + public: // supply path to the cache index file HttpMetaCache(QString path = QString()); - ~HttpMetaCache(); + ~HttpMetaCache() override; // get the entry solely from the cache // you probably don't want this, unless you have some specific caching needs. - MetaEntryPtr getEntry(QString base, QString resource_path); + auto getEntry(QString base, QString resource_path) -> MetaEntryPtr; // get the entry from cache and verify that it isn't stale (within reason) - MetaEntryPtr resolveEntry(QString base, QString resource_path, - QString expected_etag = QString()); + auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr; // add a previously resolved stale entry - bool updateEntry(MetaEntryPtr stale_entry); + auto updateEntry(MetaEntryPtr stale_entry) -> bool; // evict selected entry from cache - bool evictEntry(MetaEntryPtr entry); + auto evictEntry(MetaEntryPtr entry) -> bool; void addBase(QString base, QString base_root); // (re)start a timer that calls SaveNow later. void SaveEventually(); void Load(); - QString getBasePath(QString base); -public -slots: + + auto getBasePath(QString base) -> QString; + + public slots: void SaveNow(); -private: + private: // create a new stale entry, given the parameters - MetaEntryPtr staleEntry(QString base, QString resource_path); - struct EntryMap - { + auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr; + + struct EntryMap { QString base_path; QMap<QString, MetaEntryPtr> entry_list; }; + QMap<QString, EntryMap> m_entries; QString m_index_file; QTimer saveBatchingTimer; diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 5cdf0460..f86dd870 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "MetaCacheSink.h" #include <QFile> #include <QFileInfo> @@ -12,17 +47,13 @@ MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum) addValidator(md5sum); } -MetaCacheSink::~MetaCacheSink() -{ - // nil -} - -JobStatus MetaCacheSink::initCache(QNetworkRequest& request) +Task::State MetaCacheSink::initCache(QNetworkRequest& request) { if (!m_entry->isStale()) { - return Job_Finished; + return Task::State::Succeeded; } + // check if file exists, if it does, use its information for the request QFile current(m_filename); if(current.exists() && current.size() != 0) @@ -36,25 +67,31 @@ JobStatus MetaCacheSink::initCache(QNetworkRequest& request) request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); } } - return Job_InProgress; + + return Task::State::Running; } -JobStatus MetaCacheSink::finalizeCache(QNetworkReply & reply) +Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply) { QFileInfo output_file_info(m_filename); + if(wroteAnyData) { m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); } + m_entry->setETag(reply.rawHeader("ETag").constData()); + if (reply.hasRawHeader("Last-Modified")) { m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); } + m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); m_entry->setStale(false); APPLICATION->metacache()->updateEntry(m_entry); - return Job_Finished; + + return Task::State::Succeeded; } bool MetaCacheSink::hasLocalData() diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h index edcf7ad1..c9f7edfe 100644 --- a/launcher/net/MetaCacheSink.h +++ b/launcher/net/MetaCacheSink.h @@ -1,22 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once -#include "FileSink.h" + #include "ChecksumValidator.h" +#include "FileSink.h" #include "net/HttpMetaCache.h" namespace Net { -class MetaCacheSink : public FileSink -{ -public: /* con/des */ - MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum); - virtual ~MetaCacheSink(); - bool hasLocalData() override; +class MetaCacheSink : public FileSink { + public: + MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum); + virtual ~MetaCacheSink() = default; + + auto hasLocalData() -> bool override; -protected: /* methods */ - JobStatus initCache(QNetworkRequest & request) override; - JobStatus finalizeCache(QNetworkReply & reply) override; + protected: + auto initCache(QNetworkRequest& request) -> Task::State override; + auto finalizeCache(QNetworkReply& reply) -> Task::State override; -private: /* data */ + private: MetaEntryPtr m_entry; - ChecksumValidator * m_md5Node; + ChecksumValidator* m_md5Node; }; -} +} // namespace Net diff --git a/launcher/net/Mode.h b/launcher/net/Mode.h index 9a95f5ad..3d75981f 100644 --- a/launcher/net/Mode.h +++ b/launcher/net/Mode.h @@ -1,10 +1,5 @@ #pragma once -namespace Net -{ -enum class Mode -{ - Offline, - Online -}; +namespace Net { +enum class Mode { Offline, Online }; } diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index efb20953..729d4132 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -1,108 +1,76 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include <QObject> -#include <QUrl> -#include <memory> #include <QNetworkReply> -#include <QObjectPtr.h> +#include <QUrl> -enum JobStatus -{ - Job_NotStarted, - Job_InProgress, - Job_Finished, - Job_Failed, - Job_Aborted, - /* - * FIXME: @NUKE this confuses the task failing with us having a fallback in the form of local data. Clear up the confusion. - * Same could be true for aborted task - the presence of pre-existing result is a separate concern - */ - Job_Failed_Proceed -}; +#include "QObjectPtr.h" +#include "tasks/Task.h" -class NetAction : public QObject -{ +class NetAction : public Task { Q_OBJECT -protected: - explicit NetAction() : QObject(nullptr) {}; + protected: + explicit NetAction() : Task() {}; -public: + public: using Ptr = shared_qobject_ptr<NetAction>; - virtual ~NetAction() {}; + virtual ~NetAction() = default; - bool isRunning() const - { - return m_status == Job_InProgress; - } - bool isFinished() const - { - return m_status >= Job_Finished; - } - bool wasSuccessful() const - { - return m_status == Job_Finished || m_status == Job_Failed_Proceed; - } + QUrl url() { return m_url; } + auto index() -> int { return m_index_within_job; } - qint64 totalProgress() const - { - return m_total_progress; - } - qint64 currentProgress() const - { - return m_progress; - } - virtual bool abort() - { - return false; - } - virtual bool canAbort() - { - return false; - } - QUrl url() - { - return m_url; - } - -signals: - void started(int index); - void netActionProgress(int index, qint64 current, qint64 total); - void succeeded(int index); - void failed(int index); - void aborted(int index); - -protected slots: + protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; virtual void downloadError(QNetworkReply::NetworkError error) = 0; virtual void downloadFinished() = 0; virtual void downloadReadyRead() = 0; -public slots: - void start(shared_qobject_ptr<QNetworkAccessManager> network) { + public slots: + void startAction(shared_qobject_ptr<QNetworkAccessManager> network) + { m_network = network; - startImpl(); + executeTask(); } -protected: - virtual void startImpl() = 0; + protected: + void executeTask() override {}; -public: + public: shared_qobject_ptr<QNetworkAccessManager> m_network; /// index within the parent job, FIXME: nuke @@ -113,10 +81,4 @@ public: /// source URL QUrl m_url; - - qint64 m_progress = 0; - qint64 m_total_progress = 1; - -protected: - JobStatus m_status = Job_NotStarted; }; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 9bad89ed..bab35fa5 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -1,79 +1,179 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "NetJob.h" #include "Download.h" -#include <QDebug> +auto NetJob::addNetAction(NetAction::Ptr action) -> bool +{ + action->m_index_within_job = m_downloads.size(); + m_downloads.append(action); + part_info pi; + m_parts_progress.append(pi); + + partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress()); + + if (action->isRunning()) { + connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); }); + connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); }); + connect(action.get(), &NetAction::aborted, [this, action](){ partAborted(action->index()); }); + connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); }); + connect(action.get(), &NetAction::status, this, &NetJob::status); + } else { + m_todo.append(m_parts_progress.size() - 1); + } + + return true; +} + +auto NetJob::canAbort() const -> bool +{ + bool canFullyAbort = true; + + // can abort the downloads on the queue? + for (auto index : m_todo) { + auto part = m_downloads[index]; + canFullyAbort &= part->canAbort(); + } + // can abort the active downloads? + for (auto index : m_doing) { + auto part = m_downloads[index]; + canFullyAbort &= part->canAbort(); + } + + return canFullyAbort; +} + +void NetJob::executeTask() +{ + // hack that delays early failures so they can be caught easier + QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); +} + +auto NetJob::getFailedFiles() -> QStringList +{ + QStringList failed; + for (auto index : m_failed) { + failed.push_back(m_downloads[index]->url().toString()); + } + failed.sort(); + return failed; +} + +auto NetJob::abort() -> bool +{ + bool fullyAborted = true; + + // fail all downloads on the queue +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QSet<int> todoSet(m_todo.begin(), m_todo.end()); + m_failed.unite(todoSet); +#else + m_failed.unite(m_todo.toSet()); +#endif + m_todo.clear(); + + // abort active downloads + auto toKill = m_doing.values(); + for (auto index : toKill) { + auto part = m_downloads[index]; + fullyAborted &= part->abort(); + } + + return fullyAborted; +} void NetJob::partSucceeded(int index) { // do progress. all slots are 1 in size at least - auto &slot = parts_progress[index]; + auto& slot = m_parts_progress[index]; partProgress(index, slot.total_progress, slot.total_progress); m_doing.remove(index); m_done.insert(index); - downloads[index].get()->disconnect(this); + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partFailed(int index) { m_doing.remove(index); - auto &slot = parts_progress[index]; - if (slot.failures == 3) - { + + auto& slot = m_parts_progress[index]; + // Can try 3 times before failing by definitive + if (slot.failures == 3) { m_failed.insert(index); - } - else - { + } else { slot.failures++; m_todo.enqueue(index); } - downloads[index].get()->disconnect(this); + + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partAborted(int index) { m_aborted = true; + m_doing.remove(index); m_failed.insert(index); - downloads[index].get()->disconnect(this); + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) { - auto &slot = parts_progress[index]; + auto& slot = m_parts_progress[index]; slot.current_progress = bytesReceived; slot.total_progress = bytesTotal; int done = m_done.size(); int doing = m_doing.size(); - int all = parts_progress.size(); + int all = m_parts_progress.size(); qint64 bytesAll = 0; qint64 bytesTotalAll = 0; - for(auto & partIdx: m_doing) - { - auto part = parts_progress[partIdx]; + for (auto& partIdx : m_doing) { + auto part = m_parts_progress[partIdx]; // do not count parts with unknown/nonsensical total size - if(part.total_progress <= 0) - { + if (part.total_progress <= 0) { continue; } bytesAll += part.current_progress; @@ -85,134 +185,54 @@ void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) auto current_total = all * 1000; // HACK: make sure it never jumps backwards. // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress - if(m_current_progress == 1000) { + if (m_current_progress == 1000) { m_current_progress = inprogress; } - if(m_current_progress > current) - { + if (m_current_progress > current) { current = m_current_progress; } m_current_progress = current; setProgress(current, current_total); } -void NetJob::executeTask() -{ - // hack that delays early failures so they can be caught easier - QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); -} - void NetJob::startMoreParts() { - if(!isRunning()) - { - // this actually makes sense. You can put running downloads into a NetJob and then not start it until much later. + if (!isRunning()) { + // this actually makes sense. You can put running m_downloads into a NetJob and then not start it until much later. return; } + // OK. We are actively processing tasks, proceed. // Check for final conditions if there's nothing in the queue. - if(!m_todo.size()) - { - if(!m_doing.size()) - { - if(!m_failed.size()) - { + if (!m_todo.size()) { + if (!m_doing.size()) { + if (!m_failed.size()) { emitSucceeded(); - } - else if(m_aborted) - { + } else if (m_aborted) { emitAborted(); - } - else - { + } else { emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n"))); } } return; } - // There's work to do, try to start more parts. - while (m_doing.size() < 6) - { - if(!m_todo.size()) + + // There's work to do, try to start more parts, to a maximum of 6 concurrent ones. + while (m_doing.size() < 6) { + if (m_todo.size() == 0) return; int doThis = m_todo.dequeue(); m_doing.insert(doThis); - auto part = downloads[doThis]; - // connect signals :D - connect(part.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); - connect(part.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); - connect(part.get(), SIGNAL(aborted(int)), SLOT(partAborted(int))); - connect(part.get(), SIGNAL(netActionProgress(int, qint64, qint64)), - SLOT(partProgress(int, qint64, qint64))); - part->start(m_network); - } -} - - -QStringList NetJob::getFailedFiles() -{ - QStringList failed; - for (auto index: m_failed) - { - failed.push_back(downloads[index]->url().toString()); - } - failed.sort(); - return failed; -} -bool NetJob::canAbort() const -{ - bool canFullyAbort = true; - // can abort the waiting? - for(auto index: m_todo) - { - auto part = downloads[index]; - canFullyAbort &= part->canAbort(); - } - // can abort the active? - for(auto index: m_doing) - { - auto part = downloads[index]; - canFullyAbort &= part->canAbort(); - } - return canFullyAbort; -} + auto part = m_downloads[doThis]; -bool NetJob::abort() -{ - bool fullyAborted = true; - // fail all waiting - m_failed.unite(m_todo.toSet()); - m_todo.clear(); - // abort active - auto toKill = m_doing.toList(); - for(auto index: toKill) - { - auto part = downloads[index]; - fullyAborted &= part->abort(); - } - return fullyAborted; -} + // connect signals :D + connect(part.get(), &NetAction::succeeded, this, [this, part]{ partSucceeded(part->index()); }); + connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); }); + connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); }); + connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); }); + connect(part.get(), &NetAction::status, this, &NetJob::status); -bool NetJob::addNetAction(NetAction::Ptr action) -{ - action->m_index_within_job = downloads.size(); - downloads.append(action); - part_info pi; - parts_progress.append(pi); - partProgress(parts_progress.count() - 1, action->currentProgress(), action->totalProgress()); - - if(action->isRunning()) - { - connect(action.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); - connect(action.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); - connect(action.get(), SIGNAL(netActionProgress(int, qint64, qint64)), SLOT(partProgress(int, qint64, qint64))); + part->startAction(m_network); } - else - { - m_todo.append(parts_progress.size() - 1); - } - return true; } - -NetJob::~NetJob() = default; diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index fdea710f..63c1cf51 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -1,88 +1,98 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once + #include <QtNetwork> + +#include <QObject> #include "NetAction.h" -#include "Download.h" -#include "HttpMetaCache.h" #include "tasks/Task.h" -#include "QObjectPtr.h" -class NetJob; +// Those are included so that they are also included by anyone using NetJob +#include "net/Download.h" +#include "net/HttpMetaCache.h" -class NetJob : public Task -{ +class NetJob : public Task { Q_OBJECT -public: + + public: using Ptr = shared_qobject_ptr<NetJob>; explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : Task(), m_network(network) { setObjectName(job_name); } - virtual ~NetJob(); + virtual ~NetJob() = default; - bool addNetAction(NetAction::Ptr action); + void executeTask() override; - NetAction::Ptr operator[](int index) - { - return downloads[index]; - } - const NetAction::Ptr at(const int index) - { - return downloads.at(index); - } - NetAction::Ptr first() - { - if (downloads.size()) - return downloads[0]; - return NetAction::Ptr(); - } - int size() const - { - return downloads.size(); - } - QStringList getFailedFiles(); + auto canAbort() const -> bool override; - bool canAbort() const override; + auto addNetAction(NetAction::Ptr action) -> bool; -private slots: - void startMoreParts(); + auto operator[](int index) -> NetAction::Ptr { return m_downloads[index]; } + auto at(int index) -> const NetAction::Ptr { return m_downloads.at(index); } + auto size() const -> int { return m_downloads.size(); } + auto first() -> NetAction::Ptr { return m_downloads.size() != 0 ? m_downloads[0] : NetAction::Ptr{}; } -public slots: - virtual void executeTask() override; - virtual bool abort() override; + auto getFailedFiles() -> QStringList; + + public slots: + // Qt can't handle auto at the start for some reason? + bool abort() override; + + private slots: + void startMoreParts(); -private slots: void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); void partSucceeded(int index); void partFailed(int index); void partAborted(int index); -private: + private: shared_qobject_ptr<QNetworkAccessManager> m_network; - struct part_info - { + struct part_info { qint64 current_progress = 0; qint64 total_progress = 1; int failures = 0; }; - QList<NetAction::Ptr> downloads; - QList<part_info> parts_progress; + + QList<NetAction::Ptr> m_downloads; + QList<part_info> m_parts_progress; QQueue<int> m_todo; QSet<int> m_doing; QSet<int> m_done; diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 52b82a0e..76b86743 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,3 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> + * Copyright (C) 2022 Swirl <swurl@swurl.xyz> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "PasteUpload.h" #include "BuildConfig.h" #include "Application.h" @@ -8,8 +45,22 @@ #include <QJsonDocument> #include <QFile> -PasteUpload::PasteUpload(QWidget *window, QString text, QString url) : m_window(window), m_uploadUrl(url), m_text(text.toUtf8()) +std::array<PasteUpload::PasteTypeInfo, 4> PasteUpload::PasteTypes = { + {{"0x0.st", "https://0x0.st", ""}, + {"hastebin", "https://hst.sh", "/documents"}, + {"paste.gg", "https://paste.gg", "/api/v1/pastes"}, + {"mclo.gs", "https://api.mclo.gs", "/1/log"}}}; + +PasteUpload::PasteUpload(QWidget *window, QString text, QString baseUrl, PasteType pasteType) : m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8()) { + if (m_baseUrl == "") + m_baseUrl = PasteTypes.at(pasteType).defaultBase; + + // HACK: Paste's docs say the standard API path is at /api/<version> but the official instance paste.gg doesn't follow that?? + if (pasteType == PasteGG && m_baseUrl == PasteTypes.at(pasteType).defaultBase) + m_uploadUrl = "https://api.paste.gg/v1/pastes"; + else + m_uploadUrl = m_baseUrl + PasteTypes.at(pasteType).endpointPath; } PasteUpload::~PasteUpload() @@ -19,26 +70,76 @@ PasteUpload::~PasteUpload() void PasteUpload::executeTask() { QNetworkRequest request{QUrl(m_uploadUrl)}; - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + QNetworkReply *rep{}; - QHttpMultiPart *multiPart = new QHttpMultiPart{QHttpMultiPart::FormDataType}; + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - QHttpPart filePart; - filePart.setBody(m_text); - filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); - filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); + switch (m_pasteType) { + case NullPointer: { + QHttpMultiPart *multiPart = + new QHttpMultiPart{QHttpMultiPart::FormDataType}; - multiPart->append(filePart); + QHttpPart filePart; + filePart.setBody(m_text); + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + "form-data; name=\"file\"; filename=\"log.txt\""); + multiPart->append(filePart); - QNetworkReply *rep = APPLICATION->network()->post(request, multiPart); - multiPart->setParent(rep); + rep = APPLICATION->network()->post(request, multiPart); + multiPart->setParent(rep); - m_reply = std::shared_ptr<QNetworkReply>(rep); - setStatus(tr("Uploading to %1").arg(m_uploadUrl)); + break; + } + case Hastebin: { + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); + rep = APPLICATION->network()->post(request, m_text); + break; + } + case Mclogs: { + QUrlQuery postData; + postData.addQueryItem("content", m_text); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); + break; + } + case PasteGG: { + QJsonObject obj; + QJsonDocument doc; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + obj.insert("expires", QDateTime::currentDateTimeUtc().addDays(100).toString(Qt::DateFormat::ISODate)); + + QJsonArray files; + QJsonObject logFileInfo; + QJsonObject logFileContentInfo; + logFileContentInfo.insert("format", "text"); + logFileContentInfo.insert("value", QString::fromUtf8(m_text)); + logFileInfo.insert("name", "log.txt"); + logFileInfo.insert("content", logFileContentInfo); + files.append(logFileInfo); + + obj.insert("files", files); + + doc.setObject(obj); + rep = APPLICATION->network()->post(request, doc.toJson()); + break; + } + } connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); +#else + connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &PasteUpload::downloadError); +#endif + + + m_reply = std::shared_ptr<QNetworkReply>(rep); + + setStatus(tr("Uploading to %1").arg(m_uploadUrl)); } void PasteUpload::downloadError(QNetworkReply::NetworkError error) @@ -68,6 +169,82 @@ void PasteUpload::downloadFinished() return; } - m_pasteLink = QString::fromUtf8(data).trimmed(); + switch (m_pasteType) + { + case NullPointer: + m_pasteLink = QString::fromUtf8(data).trimmed(); + break; + case Hastebin: { + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("key") && jsonObj["key"].isString()) + { + QString key = jsonDoc.object()["key"].toString(); + m_pasteLink = m_baseUrl + "/" + key; + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } + case Mclogs: { + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("success") && jsonObj["success"].isBool()) + { + bool success = jsonObj["success"].toBool(); + if (success) + { + m_pasteLink = jsonObj["url"].toString(); + } + else + { + QString error = jsonObj["error"].toString(); + emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); + qCritical() << m_uploadUrl << " returned error: " << error; + qCritical() << "Response body: " << data; + return; + } + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } + case PasteGG: + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("status") && jsonObj["status"].isString()) + { + QString status = jsonObj["status"].toString(); + if (status == "success") + { + m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString(); + } + else + { + QString error = jsonObj["error"].toString(); + QString message = (jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none"; + emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message)); + qCritical() << m_uploadUrl << " returned error: " << error; + qCritical() << "Error message: " << message; + qCritical() << "Response body: " << data; + return; + } + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } emitSucceeded(); } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 62b2dc36..eb315c2b 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,14 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once + #include "tasks/Task.h" #include <QNetworkReply> +#include <QString> #include <QBuffer> #include <memory> +#include <array> class PasteUpload : public Task { Q_OBJECT public: - PasteUpload(QWidget *window, QString text, QString url); + enum PasteType : int { + // 0x0.st + NullPointer, + // hastebin.com + Hastebin, + // paste.gg + PasteGG, + // mclo.gs + Mclogs, + // Helpful to get the range of valid values on the enum for input sanitisation: + First = NullPointer, + Last = Mclogs + }; + + struct PasteTypeInfo { + const QString name; + const QString defaultBase; + const QString endpointPath; + }; + + static std::array<PasteTypeInfo, 4> PasteTypes; + + PasteUpload(QWidget *window, QString text, QString url, PasteType pasteType); virtual ~PasteUpload(); QString pasteLink() @@ -21,7 +81,9 @@ protected: private: QWidget *m_window; QString m_pasteLink; + QString m_baseUrl; QString m_uploadUrl; + PasteType m_pasteType; QByteArray m_text; std::shared_ptr<QNetworkReply> m_reply; public diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index d367fb15..3870f29b 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "net/NetAction.h" @@ -5,33 +40,39 @@ #include "Validator.h" namespace Net { -class Sink -{ -public: /* con/des */ - Sink() {}; - virtual ~Sink() {}; +class Sink { + public: + Sink() = default; + virtual ~Sink() = default; + + public: + virtual auto init(QNetworkRequest& request) -> Task::State = 0; + virtual auto write(QByteArray& data) -> Task::State = 0; + virtual auto abort() -> Task::State = 0; + virtual auto finalize(QNetworkReply& reply) -> Task::State = 0; -public: /* methods */ - virtual JobStatus init(QNetworkRequest & request) = 0; - virtual JobStatus write(QByteArray & data) = 0; - virtual JobStatus abort() = 0; - virtual JobStatus finalize(QNetworkReply & reply) = 0; - virtual bool hasLocalData() = 0; + virtual auto hasLocalData() -> bool = 0; - void addValidator(Validator * validator) + void addValidator(Validator* validator) { - if(validator) - { + if (validator) { validators.push_back(std::shared_ptr<Validator>(validator)); } } -protected: /* methods */ - bool finalizeAllValidators(QNetworkReply & reply) + protected: + bool initAllValidators(QNetworkRequest& request) + { + for (auto& validator : validators) { + if (!validator->init(request)) + return false; + } + return true; + } + bool finalizeAllValidators(QNetworkReply& reply) { - for(auto & validator: validators) - { - if(!validator->validate(reply)) + for (auto& validator : validators) { + if (!validator->validate(reply)) return false; } return true; @@ -39,32 +80,21 @@ protected: /* methods */ bool failAllValidators() { bool success = true; - for(auto & validator: validators) - { + for (auto& validator : validators) { success &= validator->abort(); } return success; } - bool initAllValidators(QNetworkRequest & request) - { - for(auto & validator: validators) - { - if(!validator->init(request)) - return false; - } - return true; - } - bool writeAllValidators(QByteArray & data) + bool writeAllValidators(QByteArray& data) { - for(auto & validator: validators) - { - if(!validator->write(data)) + for (auto& validator : validators) { + if (!validator->write(data)) return false; } return true; } -protected: /* data */ + protected: std::vector<std::shared_ptr<Validator>> validators; }; -} +} // namespace Net diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp new file mode 100644 index 00000000..c9942a8d --- /dev/null +++ b/launcher/net/Upload.cpp @@ -0,0 +1,199 @@ +// +// Created by timoreo on 20/05/22. +// + +#include "Upload.h" + +#include <utility> +#include "ByteArraySink.h" +#include "BuildConfig.h" +#include "Application.h" + +namespace Net { + + void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + setProgress(bytesReceived, bytesTotal); + } + + void Upload::downloadError(QNetworkReply::NetworkError error) { + if (error == QNetworkReply::OperationCanceledError) { + qCritical() << "Aborted " << m_url.toString(); + m_state = State::AbortedByUser; + } else { + // error happened during download. + qCritical() << "Failed " << m_url.toString() << " with reason " << error; + m_state = State::Failed; + } + } + + void Upload::sslErrors(const QList<QSslError> &errors) { + int i = 1; + for (const auto& error : errors) { + qCritical() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } + } + + bool Upload::handleRedirect() + { + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { + // no redirect -> it's fine to continue + return false; + } + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if (redirectBA.size() == 0) { + // empty, yet present redirect header? WTF? + return false; + } + QString redirectStr = QString::fromUtf8(redirectBA); + + if (redirectStr.startsWith("//")) { + /* + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 + */ + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } else if (redirectStr.startsWith("/")) { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); + } + + /* + * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. + * FIXME: report Qt bug for this + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if (!redirect.isValid()) { + qWarning() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qDebug() << "Fixed location header:" << redirect; + } else { + qDebug() << "Location header:" << redirect; + } + + m_url = QUrl(redirect.toString()); + qDebug() << "Following redirect to " << m_url.toString(); + startAction(m_network); + return true; + } + + void Upload::downloadFinished() { + // handle HTTP redirection first + // very unlikely for post requests, still can happen + if (handleRedirect()) { + qDebug() << "Upload redirected:" << m_url.toString(); + return; + } + + // if the download failed before this point ... + if (m_state == State::Succeeded) { + qDebug() << "Upload failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit succeeded(); + return; + } else if (m_state == State::Failed) { + qDebug() << "Upload failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } else if (m_state == State::AbortedByUser) { + qDebug() << "Upload aborted in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit aborted(); + return; + } + + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if (data.size()) { + qDebug() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); + } + + // otherwise, finalize the whole graph + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { + qDebug() << "Upload failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } + m_reply.reset(); + qDebug() << "Upload succeeded:" << m_url.toString(); + emit succeeded(); + } + + void Upload::downloadReadyRead() { + if (m_state == State::Running) { + auto data = m_reply->readAll(); + m_state = m_sink->write(data); + } + } + + void Upload::executeTask() { + setStatus(tr("Uploading %1").arg(m_url.toString())); + + if (m_state == State::AbortedByUser) { + qWarning() << "Attempt to start an aborted Upload:" << m_url.toString(); + emit aborted(); + return; + } + QNetworkRequest request(m_url); + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: + emitSucceeded(); + qDebug() << "Upload cache hit " << m_url.toString(); + return; + case State::Running: + qDebug() << "Uploading " << m_url.toString(); + break; + case State::Inactive: + case State::Failed: + emitFailed(""); + return; + case State::AbortedByUser: + emitAborted(); + return; + } + + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); + if (request.url().host().contains("api.curseforge.com")) { + request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); + } + //TODO other types of post requests ? + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QNetworkReply* rep = m_network->post(request, m_post_data); + + m_reply.reset(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); + } + + Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) { + auto* up = new Upload(); + up->m_url = std::move(url); + up->m_sink.reset(new ByteArraySink(output)); + up->m_post_data = std::move(m_post_data); + return up; + } +} // Net diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h new file mode 100644 index 00000000..ee784c6e --- /dev/null +++ b/launcher/net/Upload.h @@ -0,0 +1,31 @@ +#pragma once + +#include "NetAction.h" +#include "Sink.h" + +namespace Net { + + class Upload : public NetAction { + Q_OBJECT + + public: + static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); + + protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void sslErrors(const QList<QSslError> & errors); + void downloadFinished() override; + void downloadReadyRead() override; + + public slots: + void executeTask() override; + private: + std::unique_ptr<Sink> m_sink; + QByteArray m_post_data; + + bool handleRedirect(); + }; + +} // Net + diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index 59b72a0b..6b3d4635 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "net/NetAction.h" diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 6724950f..3b969732 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "NewsChecker.h" @@ -61,7 +81,7 @@ void NewsChecker::rssDownloadFinished() // Parse the XML. if (!doc.setContent(newsData, false, &errorMsg, &errorLine, &errorCol)) { - QString fullErrorMsg = QString("Error parsing RSS feed XML. %s at %d:%d.").arg(errorMsg, errorLine, errorCol); + QString fullErrorMsg = QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol); fail(fullErrorMsg); newsData.clear(); return; diff --git a/launcher/news/NewsEntry.cpp b/launcher/news/NewsEntry.cpp index 137703d1..cfe07e86 100644 --- a/launcher/news/NewsEntry.cpp +++ b/launcher/news/NewsEntry.cpp @@ -54,7 +54,7 @@ inline QString childValue(const QDomElement& element, const QString& childName, bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg) { QString title = childValue(element, "title", tr("Untitled")); - QString content = childValue(element, "description", tr("No content.")); + QString content = childValue(element, "content", tr("No content.")); QString link = childValue(element, "id"); entry->title = title; diff --git a/launcher/resources/multimc/128x128/instances/flame.png b/launcher/resources/multimc/128x128/instances/flame.png Binary files differindex 8a50a0b4..6482975c 100644 --- a/launcher/resources/multimc/128x128/instances/flame.png +++ b/launcher/resources/multimc/128x128/instances/flame.png diff --git a/launcher/resources/multimc/32x32/instances/flame.png b/launcher/resources/multimc/32x32/instances/flame.png Binary files differdeleted file mode 100644 index d8987338..00000000 --- a/launcher/resources/multimc/32x32/instances/flame.png +++ /dev/null diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index e22fe7ee..2337acd6 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -6,8 +6,7 @@ <!-- REDDIT logo icon, needs reddit license! --> <file>scalable/reddit-alien.svg</file> - <!-- Icon for CurseForge. Unknown license? --> - <file alias="32x32/flame.png">32x32/instances/flame.png</file> + <!-- Icon for CurseForge. CC0 --> <file alias="128x128/flame.png">128x128/instances/flame.png</file> <!-- launcher settings page --> @@ -272,7 +271,6 @@ <file>32x32/instances/ftb_logo.png</file> <file>128x128/instances/ftb_logo.png</file> - <file>32x32/instances/flame.png</file> <file>128x128/instances/flame.png</file> <file>32x32/instances/gear.png</file> diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index d5de302a..a72c32d3 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ImgurAlbumCreation.h" #include <QNetworkRequest> @@ -13,14 +49,14 @@ ImgurAlbumCreation::ImgurAlbumCreation(QList<ScreenShot::Ptr> screenshots) : NetAction(), m_screenshots(screenshots) { m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; - m_status = Job_NotStarted; + m_state = State::Inactive; } -void ImgurAlbumCreation::startImpl() +void ImgurAlbumCreation::executeTask() { - m_status = Job_InProgress; + m_state = State::Running; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); @@ -38,16 +74,20 @@ void ImgurAlbumCreation::startImpl() m_reply.reset(rep); connect(rep, &QNetworkReply::uploadProgress, this, &ImgurAlbumCreation::downloadProgress); connect(rep, &QNetworkReply::finished, this, &ImgurAlbumCreation::downloadFinished); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +#else connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +#endif } void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error) { qDebug() << m_reply->errorString(); - m_status = Job_Failed; + m_state = State::Failed; } void ImgurAlbumCreation::downloadFinished() { - if (m_status != Job_Failed) + if (m_state != State::Failed) { QByteArray data = m_reply->readAll(); m_reply.reset(); @@ -56,33 +96,32 @@ void ImgurAlbumCreation::downloadFinished() if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); - emit failed(m_index_within_job); + emitFailed(); return; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); - emit failed(m_index_within_job); + emitFailed(); return; } m_deleteHash = object.value("data").toObject().value("deletehash").toString(); m_id = object.value("data").toObject().value("id").toString(); - m_status = Job_Finished; - emit succeeded(m_index_within_job); + m_state = State::Succeeded; + emit succeeded(); return; } else { qDebug() << m_reply->readAll(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } } void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + setProgress(bytesReceived, bytesTotal); + emit progress(bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index cb048a23..0228b6e4 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -1,7 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once + #include "net/NetAction.h" #include "Screenshot.h" -#include "QObjectPtr.h" typedef shared_qobject_ptr<class ImgurAlbumCreation> ImgurAlbumCreationPtr; class ImgurAlbumCreation : public NetAction @@ -24,16 +59,14 @@ public: protected slots: - virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - virtual void downloadError(QNetworkReply::NetworkError error); - virtual void downloadFinished(); - virtual void downloadReadyRead() - { - } + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void downloadFinished() override; + void downloadReadyRead() override {} public slots: - virtual void startImpl(); + void executeTask() override; private: QList<ScreenShot::Ptr> m_screenshots; diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 76a84947..f8ac9bc2 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -1,5 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ImgurUpload.h" #include "BuildConfig.h" +#include "Application.h" #include <QNetworkRequest> #include <QHttpMultiPart> @@ -13,22 +50,22 @@ ImgurUpload::ImgurUpload(ScreenShot::Ptr shot) : NetAction(), m_shot(shot) { m_url = BuildConfig.IMGUR_BASE_URL + "upload.json"; - m_status = Job_NotStarted; + m_state = State::Inactive; } -void ImgurUpload::startImpl() +void ImgurUpload::executeTask() { finished = false; - m_status = Job_InProgress; + m_state = Task::State::Running; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QFile f(m_shot->m_file.absoluteFilePath()); if (!f.open(QFile::ReadOnly)) { - emit failed(m_index_within_job); + emitFailed(); return; } @@ -52,8 +89,11 @@ void ImgurUpload::startImpl() m_reply.reset(rep); connect(rep, &QNetworkReply::uploadProgress, this, &ImgurUpload::downloadProgress); connect(rep, &QNetworkReply::finished, this, &ImgurUpload::downloadFinished); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), - SLOT(downloadError(QNetworkReply::NetworkError))); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +#else + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +#endif } void ImgurUpload::downloadError(QNetworkReply::NetworkError error) { @@ -63,10 +103,10 @@ void ImgurUpload::downloadError(QNetworkReply::NetworkError error) qCritical() << "Double finished ImgurUpload!"; return; } - m_status = Job_Failed; + m_state = Task::State::Failed; finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); } void ImgurUpload::downloadFinished() { @@ -84,7 +124,7 @@ void ImgurUpload::downloadFinished() qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } auto object = doc.object(); @@ -93,20 +133,19 @@ void ImgurUpload::downloadFinished() qDebug() << "Screenshot upload not successful:" << doc.toJson(); finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); m_shot->m_url = object.value("data").toObject().value("link").toString(); m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); - m_status = Job_Finished; + m_state = Task::State::Succeeded; finished = true; - emit succeeded(m_index_within_job); + emit succeeded(); return; } void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + setProgress(bytesReceived, bytesTotal); + emit progress(bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index cf54f58d..404dc876 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -1,5 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once -#include "QObjectPtr.h" + #include "net/NetAction.h" #include "Screenshot.h" @@ -21,7 +56,7 @@ slots: public slots: - void startImpl() override; + void executeTask() override; private: ScreenShot::Ptr m_shot; diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index 6a3c801d..733cd444 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "settings/INIFile.h" @@ -29,7 +49,7 @@ INIFile::INIFile() QString INIFile::unescape(QString orig) { QString out; - QChar prev = 0; + QChar prev = QChar::Null; for(auto c: orig) { if(prev == '\\') @@ -42,7 +62,7 @@ QString INIFile::unescape(QString orig) out += '#'; else out += c; - prev = 0; + prev = QChar::Null; } else { @@ -52,7 +72,7 @@ QString INIFile::unescape(QString orig) continue; } out += c; - prev = 0; + prev = QChar::Null; } } return out; @@ -117,7 +137,9 @@ bool INIFile::loadFile(QString fileName) bool INIFile::loadFile(QByteArray file) { QTextStream in(file); +#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) in.setCodec("UTF-8"); +#endif QStringList lines = in.readAll().split('\n'); for (int i = 0; i < lines.count(); i++) diff --git a/launcher/settings/INIFile_test.cpp b/launcher/settings/INIFile_test.cpp index 08c2155e..d23f9fdf 100644 --- a/launcher/settings/INIFile_test.cpp +++ b/launcher/settings/INIFile_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "settings/INIFile.h" diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp new file mode 100644 index 00000000..b88cfb13 --- /dev/null +++ b/launcher/tasks/ConcurrentTask.cpp @@ -0,0 +1,144 @@ +#include "ConcurrentTask.h" + +#include <QDebug> + +ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent) + : Task(parent), m_name(task_name), m_total_max_size(max_concurrent) +{} + +ConcurrentTask::~ConcurrentTask() +{ + for (auto task : m_queue) { + if (task) + task->deleteLater(); + } +} + +auto ConcurrentTask::getStepProgress() const -> qint64 +{ + return m_stepProgress; +} + +auto ConcurrentTask::getStepTotalProgress() const -> qint64 +{ + return m_stepTotalProgress; +} + +void ConcurrentTask::addTask(Task::Ptr task) +{ + if (!isRunning()) + m_queue.append(task); + else + qWarning() << "Tried to add a task to a running concurrent task!"; +} + +void ConcurrentTask::executeTask() +{ + m_total_size = m_queue.size(); + + for (int i = 0; i < m_total_max_size; i++) + startNext(); +} + +bool ConcurrentTask::abort() +{ + if (m_doing.isEmpty()) { + // Don't call emitAborted() here, we want to bypass the 'is the task running' check + emit aborted(); + emit finished(); + + m_aborted = true; + return true; + } + + m_queue.clear(); + + m_aborted = true; + for (auto task : m_doing) + m_aborted &= task->abort(); + + if (m_aborted) + emitAborted(); + + return m_aborted; +} + +void ConcurrentTask::startNext() +{ + if (m_aborted || m_doing.count() > m_total_max_size) + return; + + if (m_queue.isEmpty() && m_doing.isEmpty()) { + emitSucceeded(); + return; + } + + if (m_queue.isEmpty()) + return; + + Task::Ptr next = m_queue.dequeue(); + + connect(next.get(), &Task::succeeded, this, [this, next] { subTaskSucceeded(next); }); + connect(next.get(), &Task::failed, this, [this, next](QString msg) { subTaskFailed(next, msg); }); + + connect(next.get(), &Task::status, this, &ConcurrentTask::subTaskStatus); + connect(next.get(), &Task::stepStatus, this, &ConcurrentTask::subTaskStatus); + + connect(next.get(), &Task::progress, this, &ConcurrentTask::subTaskProgress); + + m_doing.insert(next.get(), next); + + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + updateState(); + + next->start(); +} + +void ConcurrentTask::subTaskSucceeded(Task::Ptr task) +{ + m_done.insert(task.get(), task); + m_doing.remove(task.get()); + + disconnect(task.get(), 0, this, 0); + + updateState(); + + startNext(); +} + +void ConcurrentTask::subTaskFailed(Task::Ptr task, const QString& msg) +{ + m_done.insert(task.get(), task); + m_failed.insert(task.get(), task); + + m_doing.remove(task.get()); + + disconnect(task.get(), 0, this, 0); + + updateState(); + + startNext(); +} + +void ConcurrentTask::subTaskStatus(const QString& msg) +{ + setStepStatus(msg); +} + +void ConcurrentTask::subTaskProgress(qint64 current, qint64 total) +{ + if (total == 0) { + setProgress(0, 100); + return; + } + + m_stepProgress = current; + m_stepTotalProgress = total; +} + +void ConcurrentTask::updateState() +{ + setProgress(m_done.count(), m_total_size); + setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") + .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size))); +} diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h new file mode 100644 index 00000000..5898899d --- /dev/null +++ b/launcher/tasks/ConcurrentTask.h @@ -0,0 +1,58 @@ +#pragma once + +#include <QQueue> +#include <QSet> + +#include "tasks/Task.h" + +class ConcurrentTask : public Task { + Q_OBJECT +public: + explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6); + virtual ~ConcurrentTask(); + + inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; }; + auto getStepProgress() const -> qint64 override; + auto getStepTotalProgress() const -> qint64 override; + + inline auto getStepStatus() const -> QString override { return m_step_status; } + + void addTask(Task::Ptr task); + +public slots: + bool abort() override; + +protected +slots: + void executeTask() override; + + virtual void startNext(); + + void subTaskSucceeded(Task::Ptr); + void subTaskFailed(Task::Ptr, const QString &msg); + void subTaskStatus(const QString &msg); + void subTaskProgress(qint64 current, qint64 total); + +protected: + void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; + + virtual void updateState(); + +protected: + QString m_name; + QString m_step_status; + + QQueue<Task::Ptr> m_queue; + + QHash<Task*, Task::Ptr> m_doing; + QHash<Task*, Task::Ptr> m_done; + QHash<Task*, Task::Ptr> m_failed; + + int m_total_max_size; + int m_total_size; + + qint64 m_stepProgress = 0; + qint64 m_stepTotalProgress = 100; + + bool m_aborted = false; +}; diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 1573e476..7f03ad2e 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -33,11 +33,22 @@ void SequentialTask::executeTask() bool SequentialTask::abort() { - bool succeeded = true; - for (auto& task : m_queue) { - if (!task->abort()) succeeded = false; + if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) { + if(m_currentIndex == -1) { + // Don't call emitAborted() here, we want to bypass the 'is the task running' check + emit aborted(); + emit finished(); + } + m_queue.clear(); + return true; } + bool succeeded = m_queue[m_currentIndex]->abort(); + m_queue.clear(); + + if(succeeded) + emitAborted(); + return succeeded; } @@ -53,12 +64,18 @@ void SequentialTask::startNext() return; } Task::Ptr next = m_queue[m_currentIndex]; + connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString))); + connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); + connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(stepStatus(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64))); - connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + next->start(); } @@ -68,7 +85,7 @@ void SequentialTask::subTaskFailed(const QString& msg) } void SequentialTask::subTaskStatus(const QString& msg) { - setStepStatus(m_queue[m_currentIndex]->getStatus()); + setStepStatus(msg); } void SequentialTask::subTaskProgress(qint64 current, qint64 total) { @@ -76,7 +93,7 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total) setProgress(0, 100); return; } - setProgress(m_currentIndex, m_queue.count()); + setProgress(m_currentIndex + 1, m_queue.count()); m_stepProgress = current; m_stepTotalProgress = total; diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index 5b3c0111..e10cb6f7 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -32,13 +32,10 @@ slots: void subTaskStatus(const QString &msg); void subTaskProgress(qint64 current, qint64 total); -signals: - void stepStatus(QString status); +protected: + void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; -private: - void setStepStatus(QString status) { m_step_status = status; }; - -private: +protected: QString m_name; QString m_step_status; diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 57307b43..bb71b98c 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "Task.h" @@ -99,7 +119,7 @@ void Task::emitAborted() m_state = State::AbortedByUser; m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; - emit failed(m_failReason); + emit aborted(); emit finished(); } diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 344a024e..aafaf68c 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -1,24 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include <QObject> -#include <QString> -#include <QStringList> - #include "QObjectPtr.h" class Task : public QObject { @@ -52,6 +68,8 @@ class Task : public QObject { virtual bool canAbort() const { return false; } + auto getState() const -> State { return m_state; } + QString getStatus() { return m_status; } virtual auto getStepStatus() const -> QString { return m_status; } @@ -68,15 +86,17 @@ class Task : public QObject { signals: void started(); - virtual void progress(qint64 current, qint64 total); + void progress(qint64 current, qint64 total); void finished(); void succeeded(); + void aborted(); void failed(QString reason); void status(QString status); + void stepStatus(QString status); public slots: virtual void start(); - virtual bool abort() { return false; }; + virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; protected: virtual void executeTask() = 0; @@ -84,13 +104,13 @@ class Task : public QObject { protected slots: virtual void emitSucceeded(); virtual void emitAborted(); - virtual void emitFailed(QString reason); + virtual void emitFailed(QString reason = ""); public slots: void setStatus(const QString& status); void setProgress(qint64 current, qint64 total); - private: + protected: State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; diff --git a/launcher/tasks/Task_test.cpp b/launcher/tasks/Task_test.cpp index 9b6cc2e5..ef153a6a 100644 --- a/launcher/tasks/Task_test.cpp +++ b/launcher/tasks/Task_test.cpp @@ -1,5 +1,4 @@ #include <QTest> -#include "TestUtil.h" #include "Task.h" diff --git a/launcher/translations/POTranslator.cpp b/launcher/translations/POTranslator.cpp index 1ffcb9a4..c77ae45d 100644 --- a/launcher/translations/POTranslator.cpp +++ b/launcher/translations/POTranslator.cpp @@ -329,6 +329,11 @@ POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslat d->reload(); } +POTranslator::~POTranslator() +{ + delete d; +} + QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, int n) const { if(disambiguation) diff --git a/launcher/translations/POTranslator.h b/launcher/translations/POTranslator.h index 6d518560..1634018c 100644 --- a/launcher/translations/POTranslator.h +++ b/launcher/translations/POTranslator.h @@ -9,6 +9,7 @@ class POTranslator : public QTranslator Q_OBJECT public: explicit POTranslator(const QString& filename, QObject * parent = nullptr); + virtual ~POTranslator(); QString translate(const char * context, const char * sourceText, const char * disambiguation, int n) const override; bool isEmpty() const override; private: diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 250854d3..848b4d19 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "TranslationsModel.h" #include <QCoreApplication> @@ -17,7 +53,7 @@ #include "Application.h" -const static QLatin1Literal defaultLangCode("en_US"); +const static QLatin1String defaultLangCode("en_US"); enum class FileType { @@ -396,9 +432,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const } case Column::Completeness: { - QString text; - text.sprintf("%3.1f %%", lang.percentTranslated()); - return text; + return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); } } } @@ -667,7 +701,7 @@ void TranslationsModel::downloadTranslation(QString key) auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); - dl->m_total_progress = lang->file_size; + dl->setProgress(dl->getProgress(), lang->file_size); d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network()); d->m_dl_job->addNetAction(dl); diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 9eb658e2..5a62e4d0 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "GuiUtil.h" #include <QClipboard> @@ -16,8 +52,9 @@ QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteUrlSetting = APPLICATION->settings()->get("PastebinURL").toString(); - std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteUrlSetting)); + auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt()); + auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); dialog.execWithTask(paste.get()); if (!paste->wasSuccessful()) diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index ae765c3c..0ad8c594 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "InstanceWindow.h" @@ -97,9 +117,9 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent) // set up instance and launch process recognition { auto launchTask = m_instance->getLaunchTask(); - on_InstanceLaunchTask_changed(launchTask); - connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::on_InstanceLaunchTask_changed); - connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::on_RunningState_changed); + instanceLaunchTaskChanged(launchTask); + connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::instanceLaunchTaskChanged); + connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::runningStateChanged); } // set up instance destruction detection @@ -152,12 +172,12 @@ void InstanceWindow::on_btnLaunchMinecraftOffline_clicked() APPLICATION->launch(m_instance, false, nullptr); } -void InstanceWindow::on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask> proc) +void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc) { m_proc = proc; } -void InstanceWindow::on_RunningState_changed(bool running) +void InstanceWindow::runningStateChanged(bool running) { updateLaunchButtons(); m_container->refreshContainer(); diff --git a/launcher/ui/InstanceWindow.h b/launcher/ui/InstanceWindow.h index 1acf684e..aec07868 100644 --- a/launcher/ui/InstanceWindow.h +++ b/launcher/ui/InstanceWindow.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -55,8 +75,8 @@ slots: void on_btnKillMinecraft_clicked(); void on_btnLaunchMinecraftOffline_clicked(); - void on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask> proc); - void on_RunningState_changed(bool running); + void instanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc); + void runningStateChanged(bool running); void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); protected: diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 951fcccf..d58f158e 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1,51 +1,72 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: * - * Authors: Andrew Okin - * Peterix - * Orochimarufan <orochimarufan.x3@gmail.com> + * Copyright 2013-2021 MultiMC Contributors * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Authors: Andrew Okin + * Peterix + * Orochimarufan <orochimarufan.x3@gmail.com> * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 * - * 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. + * 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 "Application.h" #include "BuildConfig.h" #include "MainWindow.h" -#include <QtCore/QVariant> -#include <QtCore/QUrl> -#include <QtCore/QDir> -#include <QtCore/QFileInfo> - -#include <QtGui/QKeyEvent> - -#include <QtWidgets/QAction> -#include <QtWidgets/QApplication> -#include <QtWidgets/QButtonGroup> -#include <QtWidgets/QHBoxLayout> -#include <QtWidgets/QHeaderView> -#include <QtWidgets/QMainWindow> -#include <QtWidgets/QStatusBar> -#include <QtWidgets/QToolBar> -#include <QtWidgets/QWidget> -#include <QtWidgets/QMenu> -#include <QtWidgets/QMenuBar> -#include <QtWidgets/QMessageBox> -#include <QtWidgets/QInputDialog> -#include <QtWidgets/QLabel> -#include <QtWidgets/QToolButton> -#include <QtWidgets/QWidgetAction> -#include <QtWidgets/QProgressDialog> -#include <QtWidgets/QShortcut> +#include <QVariant> +#include <QUrl> +#include <QDir> +#include <QFileInfo> + +#include <QKeyEvent> +#include <QAction> + +#include <QApplication> +#include <QButtonGroup> +#include <QHBoxLayout> +#include <QHeaderView> +#include <QMainWindow> +#include <QStatusBar> +#include <QToolBar> +#include <QWidget> +#include <QMenu> +#include <QMenuBar> +#include <QMessageBox> +#include <QInputDialog> +#include <QLabel> +#include <QToolButton> +#include <QWidgetAction> +#include <QProgressDialog> +#include <QShortcut> #include <BaseInstance.h> #include <InstanceList.h> @@ -74,6 +95,7 @@ #include "ui/instanceview/InstanceDelegate.h" #include "ui/widgets/LabeledToolButton.h" #include "ui/dialogs/NewInstanceDialog.h" +#include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/VersionSelectDialog.h" @@ -203,6 +225,7 @@ public: TranslatedAction actionMoreNews; TranslatedAction actionManageAccounts; TranslatedAction actionLaunchInstance; + TranslatedAction actionKillInstance; TranslatedAction actionRenameInstance; TranslatedAction actionChangeInstGroup; TranslatedAction actionChangeInstIcon; @@ -212,7 +235,6 @@ public: TranslatedAction actionMods; TranslatedAction actionViewSelectedInstFolder; TranslatedAction actionViewSelectedMCFolder; - TranslatedAction actionViewSelectedModsFolder; TranslatedAction actionDeleteInstance; TranslatedAction actionConfig_Folder; TranslatedAction actionCAT; @@ -261,27 +283,6 @@ public: TranslatedToolbar instanceToolBar; TranslatedToolbar newsToolBar; QVector<TranslatedToolbar *> all_toolbars; - bool m_kill = false; - - void updateLaunchAction() - { - if(m_kill) - { - actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Kill")); - actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance")); - } - else - { - actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Launch")); - actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance.")); - } - actionLaunchInstance.retranslate(); - } - void setLaunchAction(bool kill) - { - m_kill = kill; - updateLaunchAction(); - } void createMainToolbarActions(MainWindow *MainWindow) { @@ -482,9 +483,12 @@ public: menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); fileMenu = menuBar->addMenu(tr("&File")); + // Workaround for QTBUG-94802 (https://bugreports.qt.io/browse/QTBUG-94802); also present for other menus + fileMenu->setSeparatorsCollapsible(false); fileMenu->addAction(actionAddInstance); fileMenu->addAction(actionLaunchInstance); fileMenu->addAction(actionLaunchInstanceOffline); + fileMenu->addAction(actionKillInstance); fileMenu->addAction(actionCloseWindow); fileMenu->addSeparator(); fileMenu->addAction(actionEditInstance); @@ -505,15 +509,18 @@ public: fileMenu->addAction(actionSettings); viewMenu = menuBar->addMenu(tr("&View")); + viewMenu->setSeparatorsCollapsible(false); viewMenu->addAction(actionCAT); viewMenu->addSeparator(); menuBar->addMenu(foldersMenu); profileMenu = menuBar->addMenu(tr("&Profiles")); + profileMenu->setSeparatorsCollapsible(false); profileMenu->addAction(actionManageAccounts); helpMenu = menuBar->addMenu(tr("&Help")); + helpMenu->setSeparatorsCollapsible(false); helpMenu->addAction(actionAbout); helpMenu->addAction(actionOpenWiki); helpMenu->addAction(actionNewsMenuBar); @@ -559,10 +566,9 @@ public: } // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) + // Actions that also require other conditions (e.g. a running instance) won't be changed. void setInstanceActionsEnabled(bool enabled) { - actionLaunchInstance->setEnabled(enabled); - actionLaunchInstanceOffline->setEnabled(enabled); actionEditInstance->setEnabled(enabled); actionEditInstNotes->setEnabled(enabled); actionMods->setEnabled(enabled); @@ -649,6 +655,14 @@ public: actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode.")); all_actions.append(&actionLaunchInstanceOffline); + actionKillInstance = TranslatedAction(MainWindow); + actionKillInstance->setObjectName(QStringLiteral("actionKillInstance")); + actionKillInstance->setDisabled(true); + actionKillInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Kill")); + actionKillInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance")); + actionKillInstance->setShortcut(QKeySequence(tr("Ctrl+K"))); + all_actions.append(&actionKillInstance); + actionEditInstance = TranslatedAction(MainWindow); actionEditInstance->setObjectName(QStringLiteral("actionEditInstance")); actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Inst&ance...")); @@ -694,14 +708,6 @@ public: actionViewSelectedMCFolder->setShortcut(QKeySequence(tr("Ctrl+M"))); all_actions.append(&actionViewSelectedMCFolder); - /* - actionViewSelectedModsFolder = TranslatedAction(MainWindow); - actionViewSelectedModsFolder->setObjectName(QStringLiteral("actionViewSelectedModsFolder")); - actionViewSelectedModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Mods Folder")); - actionViewSelectedModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's mods folder in a file browser.")); - all_actions.append(&actionViewSelectedModsFolder); - */ - actionConfig_Folder = TranslatedAction(MainWindow); actionConfig_Folder->setObjectName(QStringLiteral("actionConfig_Folder")); actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Confi&g Folder")); @@ -764,6 +770,7 @@ public: instanceToolBar->addAction(actionLaunchInstance); instanceToolBar->addAction(actionLaunchInstanceOffline); + instanceToolBar->addAction(actionKillInstance); instanceToolBar->addSeparator(); @@ -777,9 +784,6 @@ public: instanceToolBar->addSeparator(); instanceToolBar->addAction(actionViewSelectedMCFolder); - /* - instanceToolBar->addAction(actionViewSelectedModsFolder); - */ instanceToolBar->addAction(actionConfig_Folder); instanceToolBar->addAction(actionViewSelectedInstFolder); @@ -801,7 +805,7 @@ public: } MainWindow->resize(800, 600); MainWindow->setWindowIcon(APPLICATION->getThemedIcon("logo")); - MainWindow->setWindowTitle(BuildConfig.LAUNCHER_DISPLAYNAME); + MainWindow->setWindowTitle(APPLICATION->applicationDisplayName()); #ifndef QT_NO_ACCESSIBILITY MainWindow->setAccessibleName(BuildConfig.LAUNCHER_NAME); #endif @@ -836,8 +840,6 @@ public: void retranslateUi(MainWindow *MainWindow) { - QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()); - MainWindow->setWindowTitle(winTitle); // all the actions for(auto * item: all_actions) { @@ -1169,14 +1171,10 @@ void MainWindow::updateToolsMenu() QToolButton *launchButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); QToolButton *launchOfflineButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline)); - if(m_selectedInstance && m_selectedInstance->isRunning()) - { - ui->actionLaunchInstance->setMenu(nullptr); - ui->actionLaunchInstanceOffline->setMenu(nullptr); - launchButton->setPopupMode(QToolButton::InstantPopup); - launchOfflineButton->setPopupMode(QToolButton::InstantPopup); - return; - } + bool currentInstanceRunning = m_selectedInstance && m_selectedInstance->isRunning(); + + ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning); + ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning); QMenu *launchMenu = ui->actionLaunchInstance->menu(); QMenu *launchOfflineMenu = ui->actionLaunchInstanceOffline->menu(); @@ -1204,6 +1202,9 @@ void MainWindow::updateToolsMenu() normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); if (m_selectedInstance) { + normalLaunch->setEnabled(m_selectedInstance->canLaunch()); + normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch()); + connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true); }); @@ -1234,6 +1235,9 @@ void MainWindow::updateToolsMenu() } else if (m_selectedInstance) { + profilerAction->setEnabled(m_selectedInstance->canLaunch()); + profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch()); + connect(profilerAction, &QAction::triggered, [this, profiler]() { APPLICATION->launch(m_selectedInstance, true, profiler.get()); @@ -1486,7 +1490,11 @@ void MainWindow::updateNotAvailable() QList<int> stringToIntList(const QString &string) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QStringList split = string.split(',', Qt::SkipEmptyParts); +#else QStringList split = string.split(',', QString::SkipEmptyParts); +#endif QList<int> out; for (int i = 0; i < split.size(); ++i) { @@ -1876,14 +1884,12 @@ void MainWindow::globalSettingsClosed() updateMainToolBar(); updateToolsMenu(); updateStatusCenter(); + // This needs to be done to prevent UI elements disappearing in the event the config is changed + // but PolyMC exits abnormally, causing the window state to never be saved: + APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); update(); } -void MainWindow::on_actionInstanceSettings_triggered() -{ - APPLICATION->showInstanceWindow(m_selectedInstance, "settings"); -} - void MainWindow::on_actionEditInstNotes_triggered() { APPLICATION->showInstanceWindow(m_selectedInstance, "notes"); @@ -1926,20 +1932,17 @@ void MainWindow::on_actionOpenWiki_triggered() void MainWindow::on_actionMoreNews_triggered() { - DesktopServices::openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); + auto entries = m_newsChecker->getNewsEntries(); + NewsDialog news_dialog(entries, this); + news_dialog.exec(); } void MainWindow::newsButtonClicked() { - QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries(); - if (entries.count() > 0) - { - DesktopServices::openUrl(QUrl(entries[0]->link)); - } - else - { - DesktopServices::openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); - } + auto entries = m_newsChecker->getNewsEntries(); + NewsDialog news_dialog(entries, this); + news_dialog.toggleArticleList(); + news_dialog.exec(); } void MainWindow::on_actionAbout_triggered() @@ -2009,20 +2012,6 @@ void MainWindow::on_actionViewSelectedMCFolder_triggered() } } -void MainWindow::on_actionViewSelectedModsFolder_triggered() -{ - if (m_selectedInstance) - { - QString str = m_selectedInstance->modsRoot(); - if (!FS::ensureFilePathExists(str)) - { - // TODO: report error - return; - } - DesktopServices::openDirectory(QDir(str).absolutePath()); - } -} - void MainWindow::closeEvent(QCloseEvent *event) { // Save the window state and geometry. @@ -2055,15 +2044,7 @@ void MainWindow::instanceActivated(QModelIndex index) void MainWindow::on_actionLaunchInstance_triggered() { - if (!m_selectedInstance) - { - return; - } - if(m_selectedInstance->isRunning()) - { - APPLICATION->kill(m_selectedInstance); - } - else + if(m_selectedInstance && !m_selectedInstance->isRunning()) { APPLICATION->launch(m_selectedInstance); } @@ -2082,6 +2063,14 @@ void MainWindow::on_actionLaunchInstanceOffline_triggered() } } +void MainWindow::on_actionKillInstance_triggered() +{ + if(m_selectedInstance && m_selectedInstance->isRunning()) + { + APPLICATION->kill(m_selectedInstance); + } +} + void MainWindow::taskEnd() { QObject *sender = QObject::sender(); @@ -2106,23 +2095,18 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & selectionBad(); return; } + if (m_selectedInstance) { + disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); if (m_selectedInstance) { ui->instanceToolBar->setEnabled(true); ui->setInstanceActionsEnabled(true); - if(m_selectedInstance->isRunning()) - { - ui->actionLaunchInstance->setEnabled(true); - ui->setLaunchAction(true); - } - else - { - ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); - ui->setLaunchAction(false); - } + ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); + ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); ui->renameButton->setText(m_selectedInstance->name()); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); @@ -2132,11 +2116,16 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & updateToolsMenu(); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); + + connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); } else { ui->instanceToolBar->setEnabled(false); ui->setInstanceActionsEnabled(false); + ui->actionLaunchInstance->setEnabled(false); + ui->actionLaunchInstanceOffline->setEnabled(false); + ui->actionKillInstance->setEnabled(false); APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; @@ -2166,6 +2155,7 @@ void MainWindow::selectionBad() statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); ui->setInstanceActionsEnabled(false); + updateToolsMenu(); ui->renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); @@ -2221,3 +2211,9 @@ void MainWindow::updateStatusCenter() m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); } } + +void MainWindow::refreshCurrentInstance(bool running) +{ + auto current = view->selectionModel()->currentIndex(); + instanceChanged(current, current); +} diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 2032acba..d7930b5a 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -1,16 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Andrew Okin + * Peterix + * Orochimarufan <orochimarufan.x3@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -94,8 +118,6 @@ private slots: void on_actionViewSelectedMCFolder_triggered(); - void on_actionViewSelectedModsFolder_triggered(); - void refreshInstances(); void on_actionViewCentralModsFolder_triggered(); @@ -104,8 +126,6 @@ private slots: void on_actionSettings_triggered(); - void on_actionInstanceSettings_triggered(); - void on_actionManageAccounts_triggered(); void on_actionReportBug_triggered(); @@ -120,6 +140,8 @@ private slots: void on_actionLaunchInstanceOffline_triggered(); + void on_actionKillInstance_triggered(); + void on_actionDeleteInstance_triggered(); void deleteGroup(); @@ -192,6 +214,8 @@ private slots: void keyReleaseEvent(QKeyEvent *event) override; #endif + void refreshCurrentInstance(bool running); + private: void retranslateUi(); diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 8dadb755..c5367d5b 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -64,7 +64,9 @@ QString getCreditsHtml() { QString output; QTextStream stream(&output); +#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) stream.setCodec(QTextCodec::codecForName("UTF-8")); +#endif stream << "<center>\n"; //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers" diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui index 70c5009d..6323992b 100644 --- a/launcher/ui/dialogs/AboutDialog.ui +++ b/launcher/ui/dialogs/AboutDialog.ui @@ -89,9 +89,15 @@ </item> <item> <widget class="QLabel" name="versionLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> </widget> </item> <item> @@ -133,6 +139,9 @@ <property name="alignment"> <set>Qt::AlignCenter</set> </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set> + </property> </widget> </item> <item> @@ -160,32 +169,50 @@ </item> <item> <widget class="QLabel" name="platformLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> <property name="text"> <string>Platform:</string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> </widget> </item> <item> <widget class="QLabel" name="buildNumLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> <property name="text"> <string>Build Number:</string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> </widget> </item> <item> <widget class="QLabel" name="channelLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> <property name="text"> <string>Channel:</string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> </widget> </item> <item> diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index e5113981..9ec341bc 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QLayout> @@ -39,8 +59,14 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setText(original->name()); ui->instNameTextBox->setFocus(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto groupList = APPLICATION->instances()->getGroups(); + QSet<QString> groups(groupList.begin(), groupList.end()); + groupList = QStringList(groups.values()); +#else auto groups = APPLICATION->instances()->getGroups().toSet(); auto groupList = QStringList(groups.toList()); +#endif groupList.sort(Qt::CaseInsensitive); groupList.removeOne(""); groupList.push_front(""); diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 8631edf6..9f32dd8e 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -488,7 +488,11 @@ void ExportInstanceDialog::loadPackIgnore() } auto data = ignoreFile.readAll(); auto string = QString::fromUtf8(data); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + proxyModel->setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); +#else proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); +#endif } void ExportInstanceDialog::savePackIgnore() diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 305e85c0..f01c9c07 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -77,18 +77,20 @@ void ModDownloadDialog::confirm() auto keys = modTask.keys(); keys.sort(Qt::CaseInsensitive); - auto confirm_dialog = ReviewMessageBox::create( - this, - tr("Confirm mods to download") - ); + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download")); - for(auto& task : keys){ - confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename()); + for (auto& task : keys) { + confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() }); } - connect(confirm_dialog, &QDialog::accepted, this, &ModDownloadDialog::accept); + if (confirm_dialog->exec()) { + auto deselected = confirm_dialog->deselectedMods(); + for (auto name : deselected) { + modTask.remove(name); + } - confirm_dialog->open(); + this->accept(); + } } void ModDownloadDialog::accept() @@ -132,6 +134,12 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena return iter != modTask.end() && (iter.value()->getFilename() == filename); } +bool ModDownloadDialog::isModSelected(const QString &name) const +{ + auto iter = modTask.find(name); + return iter != modTask.end(); +} + ModDownloadDialog::~ModDownloadDialog() { } diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 782dc361..5c565ad3 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -32,6 +32,7 @@ public: void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); void removeSelectedMod(const QString & name = QString()); bool isModSelected(const QString & name, const QString & filename) const; + bool isModSelected(const QString & name) const; const QList<ModDownloadTask*> getTasks(); const std::shared_ptr<ModFolderModel> &mods; @@ -41,8 +42,6 @@ public slots: void accept() override; void reject() override; -//private slots: - private: Ui::ModDownloadDialog *ui = nullptr; PageContainer * m_container = nullptr; diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp index 1bbafb0c..ea790e8c 100644 --- a/launcher/ui/dialogs/NewComponentDialog.cpp +++ b/launcher/ui/dialogs/NewComponentDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "Application.h" @@ -46,7 +66,6 @@ NewComponentDialog::NewComponentDialog(const QString & initialName, const QStrin connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); - auto groups = APPLICATION->instances()->getGroups().toSet(); ui->nameTextBox->setFocus(); originalPlaceholderText = ui->uidTextBox->placeholderText(); diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 05ea091d..5b8ecc5b 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "Application.h" @@ -54,8 +74,14 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString InstIconKey = "default"; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto groupList = APPLICATION->instances()->getGroups(); + auto groups = QSet<QString>(groupList.begin(), groupList.end()); + groupList = groups.values(); +#else auto groups = APPLICATION->instances()->getGroups().toSet(); auto groupList = QStringList(groups.toList()); +#endif groupList.sort(Qt::CaseInsensitive); groupList.removeOne(""); groupList.push_front(initialGroup); diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp new file mode 100644 index 00000000..d3b21627 --- /dev/null +++ b/launcher/ui/dialogs/NewsDialog.cpp @@ -0,0 +1,49 @@ +#include "NewsDialog.h" +#include "ui_NewsDialog.h" + +NewsDialog::NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent) : QDialog(parent), ui(new Ui::NewsDialog()) +{ + ui->setupUi(this); + + for (auto entry : entries) { + ui->articleListWidget->addItem(entry->title); + m_entries.insert(entry->title, entry); + } + + connect(ui->articleListWidget, &QListWidget::currentTextChanged, this, &NewsDialog::selectedArticleChanged); + connect(ui->toggleListButton, &QPushButton::clicked, this, &NewsDialog::toggleArticleList); + + m_article_list_hidden = ui->articleListWidget->isHidden(); + + auto first_item = ui->articleListWidget->item(0); + first_item->setSelected(true); + + auto article_entry = m_entries.constFind(first_item->text()).value(); + ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, first_item->text())); + ui->currentArticleContentBrowser->setText(article_entry->content); +} + +NewsDialog::~NewsDialog() +{ + delete ui; +} + +void NewsDialog::selectedArticleChanged(const QString& new_title) +{ + auto const& article_entry = m_entries.constFind(new_title).value(); + + ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, new_title)); + ui->currentArticleContentBrowser->setText(article_entry->content); +} + +void NewsDialog::toggleArticleList() +{ + m_article_list_hidden = !m_article_list_hidden; + + ui->articleListWidget->setHidden(m_article_list_hidden); + + if (m_article_list_hidden) + ui->toggleListButton->setText(tr("Show article list")); + else + ui->toggleListButton->setText(tr("Hide article list")); +} diff --git a/launcher/ui/dialogs/NewsDialog.h b/launcher/ui/dialogs/NewsDialog.h new file mode 100644 index 00000000..add6b8dd --- /dev/null +++ b/launcher/ui/dialogs/NewsDialog.h @@ -0,0 +1,30 @@ +#pragma once + +#include <QDialog> +#include <QHash> + +#include "news/NewsEntry.h" + +namespace Ui { +class NewsDialog; +} + +class NewsDialog : public QDialog { + Q_OBJECT + + public: + NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent = nullptr); + ~NewsDialog(); + + public slots: + void toggleArticleList(); + + private slots: + void selectedArticleChanged(const QString& new_title); + + private: + Ui::NewsDialog* ui; + + QHash<QString, NewsEntryPtr> m_entries; + bool m_article_list_hidden = false; +}; diff --git a/launcher/ui/dialogs/NewsDialog.ui b/launcher/ui/dialogs/NewsDialog.ui new file mode 100644 index 00000000..2aaa08f1 --- /dev/null +++ b/launcher/ui/dialogs/NewsDialog.ui @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewsDialog</class> + <widget class="QDialog" name="NewsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>News</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="leftVerticalLayout"> + <item> + <widget class="QListWidget" name="articleListWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="rightVerticalLayout"> + <item> + <widget class="QLabel" name="articleTitleLabel"> + <property name="text"> + <string notr="true">Placeholder</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="currentArticleContentBrowser"> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="1"> + <widget class="QPushButton" name="closeButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>10</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QPushButton" name="toggleListButton"> + <property name="text"> + <string>Hide article list</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>closeButton</sender> + <signal>clicked()</signal> + <receiver>NewsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>277</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>149</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 76b6af49..64c0b924 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ProfileSetupDialog.h" @@ -18,7 +38,7 @@ #include <QPushButton> #include <QAction> -#include <QRegExpValidator> +#include <QRegularExpressionValidator> #include <QJsonDocument> #include <QDebug> @@ -39,9 +59,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg yellowIcon = APPLICATION->getThemedIcon("status-yellow"); badIcon = APPLICATION->getThemedIcon("status-bad"); - QRegExp permittedNames("[a-zA-Z0-9_]{3,16}"); + QRegularExpression permittedNames("[a-zA-Z0-9_]{3,16}"); auto nameEdit = ui->nameEdit; - nameEdit->setValidator(new QRegExpValidator(permittedNames)); + nameEdit->setValidator(new QRegularExpressionValidator(permittedNames)); nameEdit->setClearButtonEnabled(true); validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 648bd88b..e5226016 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -16,12 +16,12 @@ #include "ProgressDialog.h" #include "ui_ProgressDialog.h" -#include <QKeyEvent> #include <QDebug> +#include <QKeyEvent> #include "tasks/Task.h" -ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog) +ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog) { ui->setupUi(this); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -44,6 +44,7 @@ void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); task->abort(); + QDialog::reject(); } ProgressDialog::~ProgressDialog() @@ -53,24 +54,22 @@ ProgressDialog::~ProgressDialog() void ProgressDialog::updateSize() { - QSize qSize = QSize(480, minimumSizeHint().height()); + QSize qSize = QSize(480, minimumSizeHint().height()); resize(qSize); - setFixedSize(qSize); + setFixedSize(qSize); } -int ProgressDialog::execWithTask(Task *task) +int ProgressDialog::execWithTask(Task* task) { this->task = task; QDialog::DialogCode result; - if(!task) - { + if (!task) { qDebug() << "Programmer error: progress dialog created with null task."; return Accepted; } - if(handleImmediateResult(result)) - { + if (handleImmediateResult(result)) { return result; } @@ -78,58 +77,51 @@ int ProgressDialog::execWithTask(Task *task) connect(task, SIGNAL(started()), SLOT(onTaskStarted())); connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); - connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &))); + connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&))); + connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&))); connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); + m_is_multi_step = task->isMultiStep(); - if(!m_is_multi_step){ + if (!m_is_multi_step) { ui->globalStatusLabel->setHidden(true); ui->globalProgressBar->setHidden(true); } // if this didn't connect to an already running task, invoke start - if(!task->isRunning()) - { + if (!task->isRunning()) { task->start(); } - if(task->isRunning()) - { + if (task->isRunning()) { changeProgress(task->getProgress(), task->getTotalProgress()); changeStatus(task->getStatus()); return QDialog::exec(); - } - else if(handleImmediateResult(result)) - { + } else if (handleImmediateResult(result)) { return result; - } - else - { + } else { return QDialog::Rejected; } } // TODO: only provide the unique_ptr overloads -int ProgressDialog::execWithTask(std::unique_ptr<Task> &&task) +int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } -int ProgressDialog::execWithTask(std::unique_ptr<Task> &task) +int ProgressDialog::execWithTask(std::unique_ptr<Task>& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } -bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) { - if(task->isFinished()) - { - if(task->wasSuccessful()) - { + if (task->isFinished()) { + if (task->wasSuccessful()) { result = QDialog::Accepted; - } - else - { + } else { result = QDialog::Rejected; } return true; @@ -137,14 +129,12 @@ bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) return false; } -Task *ProgressDialog::getTask() +Task* ProgressDialog::getTask() { return task; } -void ProgressDialog::onTaskStarted() -{ -} +void ProgressDialog::onTaskStarted() {} void ProgressDialog::onTaskFailed(QString failure) { @@ -156,10 +146,11 @@ void ProgressDialog::onTaskSucceeded() accept(); } -void ProgressDialog::changeStatus(const QString &status) +void ProgressDialog::changeStatus(const QString& status) { + ui->globalStatusLabel->setText(task->getStatus()); ui->statusLabel->setText(task->getStepStatus()); - ui->globalStatusLabel->setText(status); + updateSize(); } @@ -168,27 +159,22 @@ void ProgressDialog::changeProgress(qint64 current, qint64 total) ui->globalProgressBar->setMaximum(total); ui->globalProgressBar->setValue(current); - if(!m_is_multi_step){ + if (!m_is_multi_step) { ui->taskProgressBar->setMaximum(total); ui->taskProgressBar->setValue(current); - } - else{ + } else { ui->taskProgressBar->setMaximum(task->getStepProgress()); ui->taskProgressBar->setValue(task->getStepTotalProgress()); } } -void ProgressDialog::keyPressEvent(QKeyEvent *e) +void ProgressDialog::keyPressEvent(QKeyEvent* e) { - if(ui->skipButton->isVisible()) - { - if (e->key() == Qt::Key_Escape) - { + if (ui->skipButton->isVisible()) { + if (e->key() == Qt::Key_Escape) { on_skipButton_clicked(true); return; - } - else if(e->key() == Qt::Key_Tab) - { + } else if (e->key() == Qt::Key_Tab) { ui->skipButton->setFocusPolicy(Qt::StrongFocus); ui->skipButton->setFocus(); ui->skipButton->setAutoDefault(true); @@ -199,14 +185,11 @@ void ProgressDialog::keyPressEvent(QKeyEvent *e) QDialog::keyPressEvent(e); } -void ProgressDialog::closeEvent(QCloseEvent *e) +void ProgressDialog::closeEvent(QCloseEvent* e) { - if (task && task->isRunning()) - { + if (task && task->isRunning()) { e->ignore(); - } - else - { + } else { QDialog::closeEvent(e); } } diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui index bf119a78..34ab71e3 100644 --- a/launcher/ui/dialogs/ProgressDialog.ui +++ b/launcher/ui/dialogs/ProgressDialog.ui @@ -40,6 +40,12 @@ </item> <item row="2" column="0"> <widget class="QLabel" name="statusLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="text"> <string>Task Status...</string> </property> diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 2bfd02e0..c92234a4 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -5,6 +5,9 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } ReviewMessageBox::~ReviewMessageBox() @@ -17,15 +20,33 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) return new ReviewMessageBox(parent, title, icon); } -void ReviewMessageBox::appendMod(const QString& name, const QString& filename) +void ReviewMessageBox::appendMod(ModInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); - itemTop->setText(0, name); + itemTop->setCheckState(0, Qt::CheckState::Checked); + itemTop->setText(0, info.name); auto filenameItem = new QTreeWidgetItem(itemTop); - filenameItem->setText(0, tr("Filename: %1").arg(filename)); + filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); itemTop->insertChildren(0, { filenameItem }); ui->modTreeWidget->addTopLevelItem(itemTop); } + +auto ReviewMessageBox::deselectedMods() -> QStringList +{ + QStringList list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 0; item != nullptr; ++i) { + if (item->checkState(0) == Qt::CheckState::Unchecked) { + list.append(item->text(0)); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 48742cd9..9cfa679a 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -6,17 +6,23 @@ namespace Ui { class ReviewMessageBox; } -class ReviewMessageBox final : public QDialog { +class ReviewMessageBox : public QDialog { Q_OBJECT public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; - void appendMod(const QString& name, const QString& filename); + using ModInformation = struct { + QString name; + QString filename; + }; + + void appendMod(ModInformation&& info); + auto deselectedMods() -> QStringList; ~ReviewMessageBox(); - private: + protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); Ui::ReviewMessageBox* ui; diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui index d04f3b3f..ab3bcc2f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.ui +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>300</height> + <width>500</width> + <height>350</height> </rect> </property> <property name="windowTitle"> @@ -20,24 +20,7 @@ <bool>true</bool> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>You're about to download the following mods:</string> - </property> - </widget> - </item> <item row="2" column="0"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - <item row="1" column="0"> <widget class="QTreeWidget" name="modTreeWidget"> <property name="alternatingRowColors"> <bool>true</bool> @@ -58,41 +41,33 @@ </column> </widget> </item> + <item row="1" column="0"> + <widget class="QLabel" name="explainLabel"> + <property name="text"> + <string>You're about to download the following mods:</string> + </property> + </widget> + </item> + <item row="5" column="0" rowspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="onlyCheckedLabel"> + <property name="text"> + <string>Only mods with a check will be downloaded!</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> <resources/> - <connections> - <connection> - <sender>buttonBox</sender> - <signal>accepted()</signal> - <receiver>ReviewMessageBox</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>200</x> - <y>265</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>buttonBox</sender> - <signal>rejected()</signal> - <receiver>ReviewMessageBox</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>200</x> - <y>265</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - </connections> + <connections/> </ui> diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp new file mode 100644 index 00000000..afdc4bae --- /dev/null +++ b/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -0,0 +1,15 @@ +#include "ScrollMessageBox.h" +#include "ui_ScrollMessageBox.h" + + +ScrollMessageBox::ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body) : + QDialog(parent), ui(new Ui::ScrollMessageBox) { + ui->setupUi(this); + this->setWindowTitle(title); + ui->label->setText(text); + ui->textBrowser->setText(body); +} + +ScrollMessageBox::~ScrollMessageBox() { + delete ui; +} diff --git a/launcher/ui/dialogs/ScrollMessageBox.h b/launcher/ui/dialogs/ScrollMessageBox.h new file mode 100644 index 00000000..84aa253a --- /dev/null +++ b/launcher/ui/dialogs/ScrollMessageBox.h @@ -0,0 +1,20 @@ +#pragma once + +#include <QDialog> + + +QT_BEGIN_NAMESPACE +namespace Ui { class ScrollMessageBox; } +QT_END_NAMESPACE + +class ScrollMessageBox : public QDialog { +Q_OBJECT + +public: + ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body); + + ~ScrollMessageBox() override; + +private: + Ui::ScrollMessageBox *ui; +}; diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui new file mode 100644 index 00000000..299d2ecc --- /dev/null +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ScrollMessageBox</class> + <widget class="QDialog" name="ScrollMessageBox"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>455</height> + </rect> + </property> + <property name="windowTitle"> + <string notr="true">ScrollMessageBox</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true"/> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QTextBrowser" name="textBrowser"> + <property name="acceptRichText"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ScrollMessageBox</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>425</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>227</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ScrollMessageBox</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>425</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>227</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 8d137afc..b5b78690 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include <QFileInfo> #include <QFileDialog> #include <QPainter> @@ -22,10 +57,10 @@ void SkinUploadDialog::on_buttonBox_accepted() { QString fileName; QString input = ui->skinPathTextBox->text(); - QRegExp urlPrefixMatcher("^([a-z]+)://.+$"); + QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$")); bool isLocalFile = false; // it has an URL prefix -> it is an URL - if(urlPrefixMatcher.exactMatch(input)) + if(urlPrefixMatcher.match(input).hasMatch()) { QUrl fileURL = input; if(fileURL.isValid()) diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp index ec77d146..e0c5a495 100644 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ b/launcher/ui/dialogs/UpdateDialog.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "UpdateDialog.h" #include "ui_UpdateDialog.h" #include <QDebug> @@ -58,7 +93,7 @@ QString reprocessMarkdown(QByteArray markdown) QString output = hoedown.process(markdown); // HACK: easier than customizing hoedown - output.replace(QRegExp("GH-([0-9]+)"), "<a href=\"https://github.com/PolyMC/PolyMC/issues/\\1\">GH-\\1</a>"); + output.replace(QRegularExpression("GH-([0-9]+)"), "<a href=\"https://github.com/PolyMC/PolyMC/issues/\\1\">GH-\\1</a>"); qDebug() << output; return output; } diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index b446e39d..137cc8d5 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "InstanceDelegate.h" @@ -24,7 +44,7 @@ #include "InstanceView.h" #include "BaseInstance.h" #include "InstanceList.h" -#include <xdgicon.h> +#include <QIcon> #include <QTextEdit> // Origin: Qt @@ -61,7 +81,7 @@ void drawSelectionRect(QPainter *painter, const QStyleOptionViewItem &option, painter->fillRect(rect, option.palette.brush(QPalette::Highlight)); else { - QColor backgroundColor = option.palette.color(QPalette::Background); + QColor backgroundColor = option.palette.color(QPalette::Window); backgroundColor.setAlpha(160); painter->fillRect(rect, QBrush(backgroundColor)); } @@ -142,7 +162,7 @@ void drawBadges(QPainter *painter, const QStyleOptionViewItem &option, BaseInsta return; } // FIXME: inject this. - auto icon = XdgIcon::fromTheme(it.next()); + auto icon = QIcon::fromTheme(it.next()); // opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state); const QPixmap pixmap; // itemSide diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 25aec1ab..fbeffe35 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "InstanceView.h" @@ -425,7 +445,12 @@ void InstanceView::mouseReleaseEvent(QMouseEvent *event) { emit clicked(index); } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem option; + initViewItemOption(&option); +#else QStyleOptionViewItem option = viewOptions(); +#endif if (m_pressedAlreadySelected) { option.state |= QStyle::State_Selected; @@ -461,7 +486,12 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent *event) QPersistentModelIndex persistent = index; emit doubleClicked(persistent); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem option; + initViewItemOption(&option); +#else QStyleOptionViewItem option = viewOptions(); +#endif if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) { emit activated(index); @@ -474,7 +504,12 @@ void InstanceView::paintEvent(QPaintEvent *event) QPainter painter(this->viewport()); - QStyleOptionViewItem option(viewOptions()); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem option; + initViewItemOption(&option); +#else + QStyleOptionViewItem option = viewOptions(); +#endif option.widget = this; int wpWidth = viewport()->width(); @@ -528,9 +563,9 @@ void InstanceView::paintEvent(QPaintEvent *event) #if 0 if (!m_lastDragPosition.isNull()) { - QPair<Group *, int> pair = rowDropPos(m_lastDragPosition); - Group *category = pair.first; - int row = pair.second; + std::pair<VisualGroup *, VisualGroup::HitResults> pair = rowDropPos(m_lastDragPosition); + VisualGroup *category = pair.first; + VisualGroup::HitResults row = pair.second; if (category) { int internalRow = row - category->firstItemIndex; @@ -618,7 +653,7 @@ void InstanceView::dropEvent(QDropEvent *event) { if(event->possibleActions() & Qt::MoveAction) { - QPair<VisualGroup *, VisualGroup::HitResults> dropPos = rowDropPos(event->pos()); + std::pair<VisualGroup *, VisualGroup::HitResults> dropPos = rowDropPos(event->pos()); const VisualGroup *group = dropPos.first; auto hitresult = dropPos.second; @@ -709,10 +744,18 @@ QRect InstanceView::geometryRect(const QModelIndex &index) const int x = pos.first; // int y = pos.second; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem option; + initViewItemOption(&option); +#else + QStyleOptionViewItem option = viewOptions(); +#endif + QRect out; out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); out.setLeft(m_spacing + x * (itemWidth() + m_spacing)); - out.setSize(itemDelegate()->sizeHint(viewOptions(), index)); + out.setSize(itemDelegate()->sizeHint(option, index)); geometryCache.insert(row, new QRect(out)); return out; } @@ -759,7 +802,12 @@ QPixmap InstanceView::renderToPixmap(const QModelIndexList &indices, QRect *r) c QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem option; + initViewItemOption(&option); +#else QStyleOptionViewItem option = viewOptions(); +#endif option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { @@ -770,16 +818,16 @@ QPixmap InstanceView::renderToPixmap(const QModelIndexList &indices, QRect *r) c return pixmap; } -QList<QPair<QRect, QModelIndex>> InstanceView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const +QList<std::pair<QRect, QModelIndex>> InstanceView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const { Q_ASSERT(r); QRect &rect = *r; - QList<QPair<QRect, QModelIndex>> ret; + QList<std::pair<QRect, QModelIndex>> ret; for (int i = 0; i < indices.count(); ++i) { const QModelIndex &index = indices.at(i); const QRect current = geometryRect(index); - ret += qMakePair(current, index); + ret += std::make_pair(current, index); rect |= current; } return ret; @@ -790,11 +838,11 @@ bool InstanceView::isDragEventAccepted(QDropEvent *event) return true; } -QPair<VisualGroup *, VisualGroup::HitResults> InstanceView::rowDropPos(const QPoint &pos) +std::pair<VisualGroup *, VisualGroup::HitResults> InstanceView::rowDropPos(const QPoint &pos) { VisualGroup::HitResults hitresult; auto group = categoryAt(pos + offset(), hitresult); - return qMakePair<VisualGroup*, int>(group, hitresult); + return std::make_pair(group, hitresult); } QPoint InstanceView::offset() const diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index 406362e6..ac338274 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -143,11 +163,11 @@ private: /* methods */ int calculateItemsPerRow() const; int verticalScrollToValue(const QModelIndex &index, const QRect &rect, QListView::ScrollHint hint) const; QPixmap renderToPixmap(const QModelIndexList &indices, QRect *r) const; - QList<QPair<QRect, QModelIndex>> draggablePaintPairs(const QModelIndexList &indices, QRect *r) const; + QList<std::pair<QRect, QModelIndex>> draggablePaintPairs(const QModelIndexList &indices, QRect *r) const; bool isDragEventAccepted(QDropEvent *event); - QPair<VisualGroup *, VisualGroup::HitResults> rowDropPos(const QPoint &pos); + std::pair<VisualGroup *, VisualGroup::HitResults> rowDropPos(const QPoint &pos); QPoint offset() const; }; diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 8991fb2d..e6bca17d 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "VisualGroup.h" @@ -55,7 +75,14 @@ void VisualGroup::update() positionInRow = 0; maxRowHeight = 0; } - auto itemHeight = view->itemDelegate()->sizeHint(view->viewOptions(), item).height(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem viewItemOption; + view->initViewItemOption(&viewItemOption); +#else + QStyleOptionViewItem viewItemOption = view->viewOptions(); +#endif + + auto itemHeight = view->itemDelegate()->sizeHint(viewItemOption, item).height(); if(itemHeight > maxRowHeight) { maxRowHeight = itemHeight; diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 287eb74f..b889e6f7 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (c) 2022 Lenny McLennington <lenny@sneed.church> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,16 +47,43 @@ #include "settings/SettingsObject.h" #include "tools/BaseProfiler.h" #include "Application.h" +#include "net/PasteUpload.h" +#include "BuildConfig.h" APIPage::APIPage(QWidget *parent) : QWidget(parent), ui(new Ui::APIPage) { + // This is here so you can reorder the entries in the combobox without messing stuff up + int comboBoxEntries[] = { + PasteUpload::PasteType::Mclogs, + PasteUpload::PasteType::NullPointer, + PasteUpload::PasteType::PasteGG, + PasteUpload::PasteType::Hastebin + }; + static QRegularExpression validUrlRegExp("https?://.+"); + ui->setupUi(this); - ui->urlChoices->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->urlChoices)); - ui->tabWidget->tabBar()->hide();\ + + for (auto pasteType : comboBoxEntries) { + ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType); + } + + void (QComboBox::*currentIndexChangedSignal)(int) (&QComboBox::currentIndexChanged); + connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); + // This function needs to be called even when the ComboBox's index is still in its default state. + updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); + ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); + + ui->metaURL->setPlaceholderText(BuildConfig.META_URL); + ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); + loadSettings(); + + resetBaseURLNote(); + connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLNote); + connect(ui->baseURLEntry, &QLineEdit::textEdited, this, &APIPage::resetBaseURLNote); } APIPage::~APIPage() @@ -63,22 +91,85 @@ APIPage::~APIPage() delete ui; } +void APIPage::resetBaseURLNote() +{ + ui->baseURLNote->hide(); + baseURLPasteType = ui->pasteTypeComboBox->currentIndex(); +} + +void APIPage::updateBaseURLNote(int index) +{ + if (baseURLPasteType == index) + { + ui->baseURLNote->hide(); + } + else if (!ui->baseURLEntry->text().isEmpty()) + { + ui->baseURLNote->show(); + } +} + +void APIPage::updateBaseURLPlaceholder(int index) +{ + int pasteType = ui->pasteTypeComboBox->itemData(index).toInt(); + QString pasteDefaultURL = PasteUpload::PasteTypes.at(pasteType).defaultBase; + ui->baseURLEntry->setPlaceholderText(pasteDefaultURL); +} + void APIPage::loadSettings() { auto s = APPLICATION->settings(); - QString pastebinURL = s->get("PastebinURL").toString(); - ui->urlChoices->setCurrentText(pastebinURL); + + int pasteType = s->get("PastebinType").toInt(); + QString pastebinURL = s->get("PastebinCustomAPIBase").toString(); + + ui->baseURLEntry->setText(pastebinURL); + int pasteTypeIndex = ui->pasteTypeComboBox->findData(pasteType); + if (pasteTypeIndex == -1) + { + pasteTypeIndex = ui->pasteTypeComboBox->findData(PasteUpload::PasteType::Mclogs); + ui->baseURLEntry->clear(); + } + + ui->pasteTypeComboBox->setCurrentIndex(pasteTypeIndex); + QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); + QString metaURL = s->get("MetaURLOverride").toString(); + ui->metaURL->setText(metaURL); + QString curseKey = s->get("CFKeyOverride").toString(); + ui->curseKey->setText(curseKey); + QString customUserAgent = s->get("UserAgentOverride").toString(); + ui->userAgentLineEdit->setText(customUserAgent); } void APIPage::applySettings() { auto s = APPLICATION->settings(); - QString pastebinURL = ui->urlChoices->currentText(); - s->set("PastebinURL", pastebinURL); + + s->set("PastebinType", ui->pasteTypeComboBox->currentData().toInt()); + s->set("PastebinCustomAPIBase", ui->baseURLEntry->text()); + QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); + QUrl metaURL = ui->metaURL->text(); + // Add required trailing slash + if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) + { + QString path = metaURL.path(); + path.append('/'); + metaURL.setPath(path); + } + // Don't allow HTTP, since meta is basically RCE with all the jar files. + if(!metaURL.isEmpty() && metaURL.scheme() == "http") + { + metaURL.setScheme("https"); + } + + s->set("MetaURLOverride", metaURL); + QString curseKey = ui->curseKey->text(); + s->set("CFKeyOverride", curseKey); + s->set("UserAgentOverride", ui->userAgentLineEdit->text()); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 20356009..17e62ae7 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (c) 2022 Lenny McLennington <lenny@sneed.church> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -73,6 +74,10 @@ public: void retranslate() override; private: + int baseURLPasteType; + void resetBaseURLNote(); + void updateBaseURLNote(int index); + void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index acde9aef..5327771c 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>491</width> - <height>474</height> + <width>800</width> + <height>600</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -30,56 +30,91 @@ </property> <widget class="QWidget" name="tab"> <attribute name="title"> - <string notr="true">Tab 1</string> + <string notr="true">Services</string> </attribute> <layout class="QVBoxLayout" name="verticalLayout_2"> <item> <widget class="QGroupBox" name="groupBox_paste"> <property name="title"> - <string>&Pastebin URL</string> + <string>&Pastebin Service</string> </property> <layout class="QVBoxLayout" name="verticalLayout_3"> <item> - <widget class="Line" name="line"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <widget class="QLabel" name="pasteServiceTypeLabel"> + <property name="text"> + <string>Paste Service &Type</string> + </property> + <property name="buddy"> + <cstring>pasteTypeComboBox</cstring> </property> </widget> </item> <item> - <widget class="QLabel" name="label_2"> - <property name="font"> - <font> - <pointsize>10</pointsize> - </font> + <widget class="QComboBox" name="pasteTypeComboBox"/> + </item> + <item> + <widget class="QLabel" name="baseURLLabel"> + <property name="text"> + <string>Base &URL</string> + </property> + <property name="buddy"> + <cstring>baseURLEntry</cstring> </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="baseURLEntry"> + <property name="placeholderText"> + <string/> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="baseURLNote"> <property name="text"> - <string><html><head/><body><p>Note: only input that starts with <span style=" font-weight:600;">http://</span> or <span style=" font-weight:600;">https://</span> will be accepted.</p></body></html></string> + <string>Note: you probably want to change or clear the Base URL after changing the paste service type.</string> </property> - <property name="scaledContents"> - <bool>false</bool> + <property name="wordWrap"> + <bool>true</bool> </property> </widget> </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_meta"> + <property name="title"> + <string>Meta&data Server</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> <item> - <widget class="QComboBox" name="urlChoices"> - <property name="editable"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>You can set this to a third-party metadata server to use patched libraries or other hacks.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> <bool>true</bool> </property> - <property name="insertPolicy"> - <enum>QComboBox::NoInsert</enum> + </widget> + </item> + <item> + <widget class="QLineEdit" name="metaURL"> + <property name="placeholderText"> + <string/> </property> - <item> - <property name="text"> - <string notr="true">https://0x0.st</string> - </property> - </item> </widget> </item> <item> - <widget class="QLabel" name="label"> + <widget class="QLabel" name="label_6"> <property name="text"> - <string><html><head/><body><p>Here you can choose from a predefined list of paste services, or input the URL of a different paste service of your choice, provided it supports the same protocol as 0x0.st, that is POST a file parameter to the URL and return a link in the response body.</p></body></html></string> + <string>Enter a custom URL for meta here.</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -96,19 +131,32 @@ </widget> </item> <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>API Keys</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> <widget class="QGroupBox" name="groupBox_msa"> <property name="title"> <string>&Microsoft Authentication</string> </property> <layout class="QVBoxLayout" name="verticalLayout_4"> <item> - <widget class="Line" name="line_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item> <widget class="QLabel" name="label_3"> <property name="text"> <string>Note: you probably don't need to set this if logging in via Microsoft Authentication already works.</string> @@ -148,6 +196,51 @@ </widget> </item> <item> + <widget class="QGroupBox" name="groupBox_curse"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>&CurseForge Core API</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Note: you probably don't need to set this if CurseForge already works.</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Enter a custom API Key for CurseForge here. </string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="curseKey"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="placeholderText"> + <string>(Default)</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -162,13 +255,55 @@ </item> </layout> </widget> + <widget class="QWidget" name="tab_3"> + <attribute name="title"> + <string>Miscellaneous</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QGroupBox" name="groupBox_ua"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="title"> + <string>User Agent</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QLineEdit" name="userAgentLineEdit"/> + </item> + <item> + <widget class="QLabel" name="userAgentLabel"> + <property name="text"> + <string>Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> </widget> </item> </layout> </widget> - <tabstops> - <tabstop>tabWidget</tabstop> - </tabstops> <resources/> <connections/> </ui> diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 6e1e2183..a608771e 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -73,9 +73,11 @@ AccountListPage::AccountListPage(QWidget *parent) m_accounts = APPLICATION->accounts(); ui->listView->setModel(m_accounts.get()); - ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::MigrationColumn, QHeaderView::ResizeToContents); + ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents); + ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents); ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); // Expand the account column @@ -253,19 +255,21 @@ void AccountListPage::updateButtonStates() { // If there is no selection, disable buttons that require something selected. QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - bool hasSelection = selection.size() > 0; + bool hasSelection = !selection.empty(); bool accountIsReady = false; + bool accountIsOnline = false; if (hasSelection) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); accountIsReady = !account->isActive(); + accountIsOnline = !account->isOffline(); } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady); - ui->actionDeleteSkin->setEnabled(accountIsReady); - ui->actionRefresh->setEnabled(accountIsReady); + ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if(m_accounts->defaultAccount().get() == nullptr) { ui->actionNoDefault->setEnabled(false); diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp index 436d766e..df1420ca 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ b/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -2,7 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index b5e8de6c..2cee15bf 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -95,7 +95,7 @@ void JavaPage::applySettings() // Java Settings s->set("JavaPath", ui->javaPathTextBox->text()); - s->set("JvmArgs", ui->jvmArgsTextBox->text()); + s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); @@ -120,13 +120,18 @@ void JavaPage::loadSettings() // Java Settings ui->javaPathTextBox->setText(s->get("JavaPath").toString()); - ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); + ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString()); ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); } void JavaPage::on_javaDetectBtn_clicked() { + if (JavaUtils::getJavaCheckPath().isEmpty()) { + JavaCommon::javaCheckNotFound(this); + return; + } + JavaInstallPtr java; VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); @@ -166,7 +171,7 @@ void JavaPage::on_javaTestBtn_clicked() return; } checker.reset(new JavaCommon::TestCheck( - this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(), + this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); checker->run(); diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 3e4b12a1..6ccffed4 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -150,19 +150,16 @@ <string>Java Runtime</string> </property> <layout class="QGridLayout" name="gridLayout_3"> - <item row="0" column="0"> - <widget class="QLabel" name="labelJavaPath"> + <item row="3" column="1"> + <widget class="QPushButton" name="javaDetectBtn"> <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="text"> - <string>&Java path:</string> - </property> - <property name="buddy"> - <cstring>javaPathTextBox</cstring> + <string>&Auto-detect...</string> </property> </widget> </item> @@ -175,31 +172,31 @@ </sizepolicy> </property> <property name="text"> - <string>J&VM arguments:</string> + <string>JVM arguments:</string> </property> - <property name="buddy"> - <cstring>jvmArgsTextBox</cstring> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> </property> </widget> </item> - <item row="4" column="1"> - <widget class="QCheckBox" name="skipCompatibilityCheckbox"> + <item row="0" column="0"> + <widget class="QLabel" name="labelJavaPath"> <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> - <property name="toolTip"> - <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string> - </property> <property name="text"> - <string>&Skip Java compatibility checks</string> + <string>&Java path:</string> + </property> + <property name="buddy"> + <cstring>javaPathTextBox</cstring> </property> </widget> </item> - <item row="3" column="1"> - <widget class="QPushButton" name="javaDetectBtn"> + <item row="3" column="2"> + <widget class="QPushButton" name="javaTestBtn"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <horstretch>0</horstretch> @@ -207,7 +204,7 @@ </sizepolicy> </property> <property name="text"> - <string>&Auto-detect...</string> + <string>&Test</string> </property> </widget> </item> @@ -237,22 +234,22 @@ </item> </layout> </item> - <item row="3" column="2"> - <widget class="QPushButton" name="javaTestBtn"> + <item row="4" column="1"> + <widget class="QCheckBox" name="skipCompatibilityCheckbox"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> + <property name="toolTip"> + <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string> + </property> <property name="text"> - <string>&Test</string> + <string>&Skip Java compatibility checks</string> </property> </widget> </item> - <item row="2" column="1" colspan="2"> - <widget class="QLineEdit" name="jvmArgsTextBox"/> - </item> <item row="5" column="1"> <widget class="QCheckBox" name="skipJavaWizardCheckbox"> <property name="toolTip"> @@ -263,6 +260,25 @@ </property> </widget> </item> + <item row="2" column="1" colspan="2"> + <widget class="QPlainTextEdit" name="jvmArgsTextBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>100</height> + </size> + </property> + </widget> + </item> </layout> </widget> </item> @@ -291,7 +307,6 @@ <tabstop>permGenSpinBox</tabstop> <tabstop>javaBrowseBtn</tabstop> <tabstop>javaPathTextBox</tabstop> - <tabstop>jvmArgsTextBox</tabstop> <tabstop>javaDetectBtn</tabstop> <tabstop>javaTestBtn</tabstop> <tabstop>tabWidget</tabstop> diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index 485d7fd4..fcd174bd 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index 9b321170..2fd4ab0d 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index b244b039..73ef0024 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -191,6 +191,11 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() } } +void LauncherPage::on_metadataDisableBtn_clicked() +{ + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); +} + void LauncherPage::refreshUpdateChannelList() { // Stop listening for selection changes. It's going to change a lot while we update it and @@ -354,6 +359,9 @@ void LauncherPage::applySettings() s->set("InstSortMode", "Name"); break; } + + // Mods + s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -465,6 +473,10 @@ void LauncherPage::loadSettings() { ui->sortByNameBtn->setChecked(true); } + + // Mods + ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } void LauncherPage::refreshFontPreview() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index bbf5d2fe..f38c922e 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -88,6 +88,7 @@ slots: void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); + void on_metadataDisableBtn_clicked(); /*! * Updates the list of update channels in the combo box. diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index a306a91b..645f7ef6 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -94,19 +94,13 @@ <string>Folders</string> </property> <layout class="QGridLayout" name="foldersBoxLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="labelInstDir"> + <item row="1" column="2"> + <widget class="QToolButton" name="modsDirBrowseBtn"> <property name="text"> - <string>I&nstances:</string> - </property> - <property name="buddy"> - <cstring>instDirTextBox</cstring> + <string notr="true">...</string> </property> </widget> </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="instDirTextBox"/> - </item> <item row="0" column="2"> <widget class="QToolButton" name="instDirBrowseBtn"> <property name="text"> @@ -114,43 +108,78 @@ </property> </widget> </item> - <item row="1" column="0"> - <widget class="QLabel" name="labelModsDir"> + <item row="2" column="2"> + <widget class="QToolButton" name="iconsDirBrowseBtn"> <property name="text"> - <string>&Mods:</string> + <string notr="true">...</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="instDirTextBox"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="labelIconsDir"> + <property name="text"> + <string>&Icons:</string> </property> <property name="buddy"> - <cstring>modsDirTextBox</cstring> + <cstring>iconsDirTextBox</cstring> </property> </widget> </item> <item row="1" column="1"> <widget class="QLineEdit" name="modsDirTextBox"/> </item> - <item row="1" column="2"> - <widget class="QToolButton" name="modsDirBrowseBtn"> + <item row="0" column="0"> + <widget class="QLabel" name="labelInstDir"> <property name="text"> - <string notr="true">...</string> + <string>I&nstances:</string> + </property> + <property name="buddy"> + <cstring>instDirTextBox</cstring> </property> </widget> </item> <item row="2" column="1"> <widget class="QLineEdit" name="iconsDirTextBox"/> </item> - <item row="2" column="0"> - <widget class="QLabel" name="labelIconsDir"> + <item row="1" column="0"> + <widget class="QLabel" name="labelModsDir"> <property name="text"> - <string>&Icons:</string> + <string>&Mods:</string> </property> <property name="buddy"> - <cstring>iconsDirTextBox</cstring> + <cstring>modsDirTextBox</cstring> </property> </widget> </item> - <item row="2" column="2"> - <widget class="QToolButton" name="iconsDirBrowseBtn"> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="modsBox"> + <property name="title"> + <string>Mods</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="metadataDisableBtn"> + <property name="toolTip"> + <string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string> + </property> <property name="text"> - <string notr="true">...</string> + <string>Disable using metadata for mods?</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="metadataWarningLabel"> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some upcoming QoL features, such as mod updating!</span></p></body></html></string> + </property> + <property name="wordWrap"> + <bool>true</bool> </property> </widget> </item> diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index f49f5a92..e3ac7e7c 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -87,6 +87,11 @@ void MinecraftPage::applySettings() s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + // Peformance related options + s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); + s->set("EnableMangoHud", ui->enableMangoHud->isChecked()); + s->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); + // Game time s->set("ShowGameTime", ui->showGameTime->isChecked()); s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); @@ -109,6 +114,14 @@ void MinecraftPage::loadSettings() ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); + ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool()); + ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool()); + ui->useDiscreteGpuCheck->setChecked(s->get("UseDiscreteGpu").toBool()); + +#if !defined(Q_OS_LINUX) + ui->perfomanceGroupBox->setVisible(false); +#endif + ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 353390bd..640f436d 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -135,6 +135,45 @@ </widget> </item> <item> + <widget class="QGroupBox" name="perfomanceGroupBox"> + <property name="title"> + <string>Performance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="enableFeralGamemodeCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html></string> + </property> + <property name="text"> + <string>Enable Feral GameMode</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="enableMangoHud"> + <property name="toolTip"> + <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> + </property> + <property name="text"> + <string>Enable MangoHud</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></string> + </property> + <property name="text"> + <string>Use discrete GPU</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <widget class="QGroupBox" name="gameTimeGroupBox"> <property name="title"> <string>Game time</string> @@ -181,15 +220,15 @@ </widget> </item> <item> - <widget class="QCheckBox" name="quitAfterGameStopCheck"> - <property name="toolTip"> - <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> - </property> - <property name="text"> - <string>&Quit the launcher after game window closes</string> - </property> - </widget> - </item> + <widget class="QCheckBox" name="quitAfterGameStopCheck"> + <property name="toolTip"> + <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> + </property> + <property name="text"> + <string>&Quit the launcher after game window closes</string> + </property> + </widget> + </item> </layout> </widget> </item> @@ -218,6 +257,9 @@ <tabstop>windowHeightSpinBox</tabstop> <tabstop>useNativeGLFWCheck</tabstop> <tabstop>useNativeOpenALCheck</tabstop> + <tabstop>enableFeralGamemodeCheck</tabstop> + <tabstop>enableMangoHud</tabstop> + <tabstop>useDiscreteGpuCheck</tabstop> </tabstops> <resources/> <connections/> diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index aefd1e74..ffff8456 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,11 +37,11 @@ #include "ProxyPage.h" #include "ui_ProxyPage.h" +#include <QButtonGroup> #include <QTabBar> #include "settings/SettingsObject.h" #include "Application.h" -#include "Application.h" ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage) { @@ -49,7 +50,8 @@ ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage) loadSettings(); updateCheckboxStuff(); - connect(ui->proxyGroup, SIGNAL(buttonClicked(int)), SLOT(proxyChanged(int))); + connect(ui->proxyGroup, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), + this, &ProxyPage::proxyGroupChanged); } ProxyPage::~ProxyPage() @@ -65,13 +67,13 @@ bool ProxyPage::apply() void ProxyPage::updateCheckboxStuff() { - ui->proxyAddrBox->setEnabled(!ui->proxyNoneBtn->isChecked() && - !ui->proxyDefaultBtn->isChecked()); - ui->proxyAuthBox->setEnabled(!ui->proxyNoneBtn->isChecked() && - !ui->proxyDefaultBtn->isChecked()); + bool enableEditing = ui->proxyHTTPBtn->isChecked() + || ui->proxySOCKS5Btn->isChecked(); + ui->proxyAddrBox->setEnabled(enableEditing); + ui->proxyAuthBox->setEnabled(enableEditing); } -void ProxyPage::proxyChanged(int) +void ProxyPage::proxyGroupChanged(QAbstractButton *button) { updateCheckboxStuff(); } diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h index e3677774..279a9029 100644 --- a/launcher/ui/pages/global/ProxyPage.h +++ b/launcher/ui/pages/global/ProxyPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,6 +37,7 @@ #pragma once #include <memory> +#include <QAbstractButton> #include <QDialog> #include "ui/pages/BasePage.h" @@ -73,15 +75,14 @@ public: bool apply() override; void retranslate() override; +private slots: + void proxyGroupChanged(QAbstractButton *button); + private: void updateCheckboxStuff(); void applySettings(); void loadSettings(); -private -slots: - void proxyChanged(int); - private: Ui::ProxyPage *ui; }; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp new file mode 100644 index 00000000..d06f412b --- /dev/null +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -0,0 +1,297 @@ +#include "ExternalResourcesPage.h" +#include "ui_ExternalResourcesPage.h" + +#include "DesktopServices.h" +#include "Version.h" +#include "minecraft/mod/ModFolderModel.h" +#include "ui/GuiUtil.h" + +#include <QKeyEvent> +#include <QMenu> + +namespace { +// FIXME: wasteful +void RemoveThePrefix(QString& string) +{ + QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); + string.remove(regex); + string = string.trimmed(); +} +} // namespace + +class SortProxy : public QSortFilterProxyModel { + public: + explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} + + protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override + { + ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel()); + if (!model) + return false; + + const auto& mod = model->at(source_row); + + if (mod.name().contains(filterRegularExpression())) + return true; + if (mod.description().contains(filterRegularExpression())) + return true; + + for (auto& author : mod.authors()) { + if (author.contains(filterRegularExpression())) { + return true; + } + } + + return false; + } + + bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override + { + ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel()); + if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + + // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and + // proceed. + + auto column = (ModFolderModel::Columns) source_left.column(); + bool invert = false; + switch (column) { + // GH-2550 - sort by enabled/disabled + case ModFolderModel::ActiveColumn: { + auto dataL = source_left.data(Qt::CheckStateRole).toBool(); + auto dataR = source_right.data(Qt::CheckStateRole).toBool(); + if (dataL != dataR) + return dataL > dataR; + + // fallthrough + invert = sortOrder() == Qt::DescendingOrder; + } + // GH-2722 - sort mod names in a way that discards "The" prefixes + case ModFolderModel::NameColumn: { + auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString(); + RemoveThePrefix(dataL); + auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString(); + RemoveThePrefix(dataR); + + auto less = dataL.compare(dataR, sortCaseSensitivity()); + if (less != 0) + return invert ? (less > 0) : (less < 0); + + // fallthrough + invert = sortOrder() == Qt::DescendingOrder; + } + // GH-2762 - sort versions by parsing them as versions + case ModFolderModel::VersionColumn: { + auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString()); + auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString()); + return invert ? (dataL > dataR) : (dataL < dataR); + } + default: { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + } + } +}; + +ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent) + : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model) +{ + ui->setupUi(this); + + runningStateChanged(m_instance && m_instance->isRunning()); + + ui->actionsToolbar->insertSpacer(ui->actionViewConfigs); + + m_filterModel = new SortProxy(this); + m_filterModel->setDynamicSortFilter(true); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSourceModel(m_model.get()); + m_filterModel->setFilterKeyColumn(-1); + ui->treeView->setModel(m_filterModel); + + ui->treeView->installEventFilter(this); + ui->treeView->sortByColumn(1, Qt::AscendingOrder); + ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); + + // The default function names by Qt are pretty ugly, so let's just connect the actions manually, + // to make it easier to read :) + connect(ui->actionAddItem, &QAction::triggered, this, &ExternalResourcesPage::addItem); + connect(ui->actionRemoveItem, &QAction::triggered, this, &ExternalResourcesPage::removeItem); + connect(ui->actionEnableItem, &QAction::triggered, this, &ExternalResourcesPage::enableItem); + connect(ui->actionDisableItem, &QAction::triggered, this, &ExternalResourcesPage::disableItem); + connect(ui->actionViewConfigs, &QAction::triggered, this, &ExternalResourcesPage::viewConfigs); + connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder); + + connect(ui->treeView, &ModListView::customContextMenuRequested, this, &ExternalResourcesPage::ShowContextMenu); + connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated); + + auto selection_model = ui->treeView->selectionModel(); + connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + connect(m_instance, &BaseInstance::runningStatusChanged, this, &ExternalResourcesPage::runningStateChanged); +} + +ExternalResourcesPage::~ExternalResourcesPage() +{ + m_model->stopWatching(); + delete ui; +} + +void ExternalResourcesPage::itemActivated(const QModelIndex&) +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle); +} + +QMenu* ExternalResourcesPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction()); + return filteredMenu; +} + +void ExternalResourcesPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->treeView->mapToGlobal(pos)); + delete menu; +} + +void ExternalResourcesPage::openedImpl() +{ + m_model->startWatching(); +} + +void ExternalResourcesPage::closedImpl() +{ + m_model->stopWatching(); +} + +void ExternalResourcesPage::retranslate() +{ + ui->retranslateUi(this); +} + +void ExternalResourcesPage::filterTextChanged(const QString& newContents) +{ + m_viewFilter = newContents; + m_filterModel->setFilterFixedString(m_viewFilter); +} + +void ExternalResourcesPage::runningStateChanged(bool running) +{ + if (m_controlsEnabled == !running) + return; + + m_controlsEnabled = !running; + ui->actionAddItem->setEnabled(m_controlsEnabled); + ui->actionDisableItem->setEnabled(m_controlsEnabled); + ui->actionEnableItem->setEnabled(m_controlsEnabled); + ui->actionRemoveItem->setEnabled(m_controlsEnabled); +} + +bool ExternalResourcesPage::shouldDisplay() const +{ + return true; +} + +bool ExternalResourcesPage::listFilter(QKeyEvent* keyEvent) +{ + switch (keyEvent->key()) { + case Qt::Key_Delete: + removeItem(); + return true; + case Qt::Key_Plus: + addItem(); + return true; + default: + break; + } + return QWidget::eventFilter(ui->treeView, keyEvent); +} + +bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev) +{ + if (ev->type() != QEvent::KeyPress) + return QWidget::eventFilter(obj, ev); + + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev); + if (obj == ui->treeView) + return listFilter(keyEvent); + + return QWidget::eventFilter(obj, ev); +} + +void ExternalResourcesPage::addItem() +{ + if (!m_controlsEnabled) + return; + + + auto list = GuiUtil::BrowseForFiles( + helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()), + m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + + if (!list.isEmpty()) { + for (auto filename : list) { + m_model->installMod(filename); + } + } +} + +void ExternalResourcesPage::removeItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->deleteMods(selection.indexes()); +} + +void ExternalResourcesPage::enableItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setModStatus(selection.indexes(), ModFolderModel::Enable); +} + +void ExternalResourcesPage::disableItem() +{ + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setModStatus(selection.indexes(), ModFolderModel::Disable); +} + +void ExternalResourcesPage::viewConfigs() +{ + DesktopServices::openDirectory(m_instance->instanceConfigFolder(), true); +} + +void ExternalResourcesPage::viewFolder() +{ + DesktopServices::openDirectory(m_model->dir().absolutePath(), true); +} + +void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) +{ + if (!current.isValid()) { + ui->frame->clear(); + return; + } + + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + Mod& m = m_model->operator[](row); + ui->frame->updateWithMod(m); +} diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h new file mode 100644 index 00000000..41237139 --- /dev/null +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -0,0 +1,73 @@ +#pragma once + +#include <QMainWindow> +#include <QSortFilterProxyModel> + +#include "Application.h" +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" + +class ModFolderModel; + +namespace Ui { +class ExternalResourcesPage; +} + +/* This page is used as a base for pages in which the user can manage external resources + * related to the game, such as mods, shaders or resource packs. */ +class ExternalResourcesPage : public QMainWindow, public BasePage { + Q_OBJECT + + public: + // FIXME: Switch to different model (or change the name of this one) + explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr); + virtual ~ExternalResourcesPage(); + + virtual QString displayName() const override = 0; + virtual QIcon icon() const override = 0; + virtual QString id() const override = 0; + virtual QString helpPage() const override = 0; + + virtual bool shouldDisplay() const override = 0; + + void openedImpl() override; + void closedImpl() override; + + void retranslate() override; + + protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + bool listFilter(QKeyEvent* ev); + QMenu* createPopupMenu() override; + + public slots: + void current(const QModelIndex& current, const QModelIndex& previous); + + protected slots: + void itemActivated(const QModelIndex& index); + void filterTextChanged(const QString& newContents); + void runningStateChanged(bool running); + + virtual void addItem(); + virtual void removeItem(); + + virtual void enableItem(); + virtual void disableItem(); + + virtual void viewFolder(); + virtual void viewConfigs(); + + void ShowContextMenu(const QPoint& pos); + + protected: + BaseInstance* m_instance = nullptr; + + Ui::ExternalResourcesPage* ui = nullptr; + std::shared_ptr<ModFolderModel> m_model; + QSortFilterProxyModel* m_filterModel = nullptr; + + QString m_fileSelectionFilter; + QString m_viewFilter; + + bool m_controlsEnabled = true; +}; diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index ab59b0df..17bf455a 100644 --- a/launcher/ui/pages/instance/ModFolderPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>ModFolderPage</class> - <widget class="QMainWindow" name="ModFolderPage"> + <class>ExternalResourcesPage</class> + <widget class="QMainWindow" name="ExternalResourcesPage"> <property name="geometry"> <rect> <x>0</x> @@ -53,7 +53,7 @@ </widget> </item> <item row="1" column="1" colspan="3"> - <widget class="ModListView" name="modTreeView"> + <widget class="ModListView" name="treeView"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <horstretch>0</horstretch> @@ -83,15 +83,15 @@ <attribute name="toolBarBreak"> <bool>false</bool> </attribute> - <addaction name="actionAdd"/> + <addaction name="actionAddItem"/> <addaction name="separator"/> - <addaction name="actionRemove"/> - <addaction name="actionEnable"/> - <addaction name="actionDisable"/> - <addaction name="actionView_configs"/> - <addaction name="actionView_Folder"/> + <addaction name="actionRemoveItem"/> + <addaction name="actionEnableItem"/> + <addaction name="actionDisableItem"/> + <addaction name="actionViewConfigs"/> + <addaction name="actionViewFolder"/> </widget> - <action name="actionAdd"> + <action name="actionAddItem"> <property name="text"> <string>&Add</string> </property> @@ -99,31 +99,31 @@ <string>Add</string> </property> </action> - <action name="actionRemove"> + <action name="actionRemoveItem"> <property name="text"> <string>&Remove</string> </property> <property name="toolTip"> - <string>Remove selected mods</string> + <string>Remove selected item</string> </property> </action> - <action name="actionEnable"> + <action name="actionEnableItem"> <property name="text"> <string>&Enable</string> </property> <property name="toolTip"> - <string>Enable selected mods</string> + <string>Enable selected item</string> </property> </action> - <action name="actionDisable"> + <action name="actionDisableItem"> <property name="text"> <string>&Disable</string> </property> <property name="toolTip"> - <string>Disable selected mods</string> + <string>Disable selected item</string> </property> </action> - <action name="actionView_configs"> + <action name="actionViewConfigs"> <property name="text"> <string>View &Configs</string> </property> @@ -131,11 +131,22 @@ <string>Open the 'config' folder in the system file manager.</string> </property> </action> - <action name="actionView_Folder"> + <action name="actionViewFolder"> <property name="text"> <string>View &Folder</string> </property> </action> + <action name="actionDownloadItem"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>&Download</string> + </property> + <property name="toolTip"> + <string>Download a new resource</string> + </property> + </action> </widget> <customwidgets> <customwidget> @@ -156,7 +167,7 @@ </customwidget> </customwidgets> <tabstops> - <tabstop>modTreeView</tabstop> + <tabstop>treeView</tabstop> <tabstop>filterEdit</tabstop> </tabstops> <resources/> diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index b4562843..fcc110de 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -2,7 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,6 +50,7 @@ #include "Application.h" #include "java/JavaInstallList.h" +#include "java/JavaUtils.h" #include "FileSystem.h" @@ -232,6 +233,22 @@ void InstanceSettingsPage::applySettings() m_settings->reset("UseNativeGLFW"); } + // Performance + bool performance = ui->perfomanceGroupBox->isChecked(); + m_settings->set("OverridePerformance", performance); + if(performance) + { + m_settings->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); + m_settings->set("EnableMangoHud", ui->enableMangoHud->isChecked()); + m_settings->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); + } + else + { + m_settings->reset("EnableFeralGamemode"); + m_settings->reset("EnableMangoHud"); + m_settings->reset("UseDiscreteGpu"); + } + // Game time bool gameTime = ui->gameTimeGroupBox->isChecked(); m_settings->set("OverrideGameTime", gameTime); @@ -325,6 +342,16 @@ void InstanceSettingsPage::loadSettings() ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); + // Performance + ui->perfomanceGroupBox->setChecked(m_settings->get("OverridePerformance").toBool()); + ui->enableFeralGamemodeCheck->setChecked(m_settings->get("EnableFeralGamemode").toBool()); + ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool()); + ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool()); + + #if !defined(Q_OS_LINUX) + ui->perfomanceGroupBox->setVisible(false); + #endif + // Miscellanous ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool()); ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool()); @@ -336,6 +363,11 @@ void InstanceSettingsPage::loadSettings() void InstanceSettingsPage::on_javaDetectBtn_clicked() { + if (JavaUtils::getJavaCheckPath().isEmpty()) { + JavaCommon::javaCheckNotFound(this); + return; + } + JavaInstallPtr java; VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index cb66b3ce..8b3c3370 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -455,6 +455,74 @@ </item> </layout> </widget> + <widget class="QWidget" name="performancePage"> + <attribute name="title"> + <string>Performance</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_14"> + <item> + <widget class="QGroupBox" name="perfomanceGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Performance</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_13"> + <item> + <widget class="QCheckBox" name="enableFeralGamemodeCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html></string> + </property> + <property name="text"> + <string>Enable Feral GameMode</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="enableMangoHud"> + <property name="toolTip"> + <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> + </property> + <property name="text"> + <string>Enable MangoHud</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></string> + </property> + <property name="text"> + <string>Use discrete GPU</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> <widget class="QWidget" name="miscellaneousPage"> <attribute name="title"> <string>Miscellaneous</string> diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 30a8735f..3d9fb025 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -62,7 +63,7 @@ public: { case Qt::FontRole: return m_font; - case Qt::TextColorRole: + case Qt::ForegroundRole: { MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); return m_colors->getFront(level); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8113fe85..4432ccc8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,409 +35,123 @@ */ #include "ModFolderPage.h" -#include "ui_ModFolderPage.h" +#include "ui_ExternalResourcesPage.h" -#include <QMessageBox> +#include <QAbstractItemModel> #include <QEvent> #include <QKeyEvent> -#include <QAbstractItemModel> #include <QMenu> +#include <QMessageBox> #include <QSortFilterProxyModel> #include "Application.h" +#include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ModDownloadDialog.h" -#include "ui/GuiUtil.h" #include "DesktopServices.h" -#include "minecraft/mod/ModFolderModel.h" -#include "minecraft/mod/Mod.h" -#include "minecraft/VersionFilterData.h" #include "minecraft/PackProfile.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModAPI.h" #include "Version.h" +#include "tasks/ConcurrentTask.h" #include "ui/dialogs/ProgressDialog.h" -#include "tasks/SequentialTask.h" - -namespace { - // FIXME: wasteful - void RemoveThePrefix(QString & string) { - QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +")); - string.remove(regex); - string = string.trimmed(); - } -} - -class ModSortProxy : public QSortFilterProxyModel -{ -public: - explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent) - { - } - -protected: - bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override { - ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel()); - if(!model) { - return false; - } - const auto &mod = model->at(source_row); - if(mod.name().contains(filterRegExp())) { - return true; - } - if(mod.description().contains(filterRegExp())) { - return true; - } - for(auto & author: mod.authors()) { - if (author.contains(filterRegExp())) { - return true; - } - } - return false; - } - - bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override - { - ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel()); - if( - !model || - !source_left.isValid() || - !source_right.isValid() || - source_left.column() != source_right.column() - ) { - return QSortFilterProxyModel::lessThan(source_left, source_right); - } - - // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed. - auto column = (ModFolderModel::Columns) source_left.column(); - bool invert = false; - switch(column) { - // GH-2550 - sort by enabled/disabled - case ModFolderModel::ActiveColumn: { - auto dataL = source_left.data(Qt::CheckStateRole).toBool(); - auto dataR = source_right.data(Qt::CheckStateRole).toBool(); - if(dataL != dataR) { - return dataL > dataR; - } - // fallthrough - invert = sortOrder() == Qt::DescendingOrder; - } - // GH-2722 - sort mod names in a way that discards "The" prefixes - case ModFolderModel::NameColumn: { - auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString(); - RemoveThePrefix(dataL); - auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString(); - RemoveThePrefix(dataR); - - auto less = dataL.compare(dataR, sortCaseSensitivity()); - if(less != 0) { - return invert ? (less > 0) : (less < 0); - } - // fallthrough - invert = sortOrder() == Qt::DescendingOrder; - } - // GH-2762 - sort versions by parsing them as versions - case ModFolderModel::VersionColumn: { - auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString()); - auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString()); - return invert ? (dataL > dataR) : (dataL < dataR); - } - default: { - return QSortFilterProxyModel::lessThan(source_left, source_right); - } - } - } -}; - -ModFolderPage::ModFolderPage( - BaseInstance *inst, - std::shared_ptr<ModFolderModel> mods, - QString id, - QString iconName, - QString displayName, - QString helpPage, - QWidget *parent -) : - QMainWindow(parent), - ui(new Ui::ModFolderPage) +ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent) + : ExternalResourcesPage(inst, mods, parent) { - ui->setupUi(this); - // This is structured like that so that these changes - // do not affect the Resouce pack and Shader pack tabs - if(id == "mods") { - auto act = new QAction(tr("Download mods"), this); - act->setToolTip(tr("Download mods from online mod platforms")); - ui->actionsToolbar->insertActionBefore(ui->actionAdd, act); - connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered); - - ui->actionAdd->setText(tr("Add .jar")); - ui->actionAdd->setToolTip(tr("Add mods via local file")); - } - - ui->actionsToolbar->insertSpacer(ui->actionView_configs); - - m_inst = inst; - on_RunningState_changed(m_inst && m_inst->isRunning()); - m_mods = mods; - m_id = id; - m_displayName = displayName; - m_iconName = iconName; - m_helpName = helpPage; - m_fileSelectionFilter = "%1 (*.zip *.jar)"; - m_filterModel = new ModSortProxy(this); - m_filterModel->setDynamicSortFilter(true); - m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); - m_filterModel->setSourceModel(m_mods.get()); - m_filterModel->setFilterKeyColumn(-1); - ui->modTreeView->setModel(m_filterModel); - ui->modTreeView->installEventFilter(this); - ui->modTreeView->sortByColumn(1, Qt::AscendingOrder); - ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu); - connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated); + // do not affect the Resource pack and Shader pack tabs + { + ui->actionDownloadItem->setText(tr("Download mods")); + ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); + ui->actionDownloadItem->setEnabled(true); + ui->actionAddItem->setText(tr("Add file")); + ui->actionAddItem->setToolTip(tr("Add a locally downloaded file")); - auto smodel = ui->modTreeView->selectionModel(); - connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent); - connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged); - connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed); -} + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); -void ModFolderPage::modItemActivated(const QModelIndex&) -{ - if(!m_controlsEnabled) { - return; + connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods); } - auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); - m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle); -} - -QMenu * ModFolderPage::createPopupMenu() -{ - QMenu* filteredMenu = QMainWindow::createPopupMenu(); - filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() ); - return filteredMenu; -} - -void ModFolderPage::ShowContextMenu(const QPoint& pos) -{ - auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu")); - menu->exec(ui->modTreeView->mapToGlobal(pos)); - delete menu; -} - -void ModFolderPage::openedImpl() -{ - m_mods->startWatching(); -} - -void ModFolderPage::closedImpl() -{ - m_mods->stopWatching(); -} - -void ModFolderPage::on_filterTextChanged(const QString& newContents) -{ - m_viewFilter = newContents; - m_filterModel->setFilterFixedString(m_viewFilter); -} - - -CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, - QString id, QString iconName, QString displayName, - QString helpPage, QWidget *parent) - : ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent) -{ } -ModFolderPage::~ModFolderPage() -{ - m_mods->stopWatching(); - delete ui; -} - -void ModFolderPage::on_RunningState_changed(bool running) -{ - if(m_controlsEnabled == !running) { - return; - } - m_controlsEnabled = !running; - ui->actionsToolbar->setEnabled(m_controlsEnabled); -} +CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent) + : ModFolderPage(inst, mods, parent) +{} bool ModFolderPage::shouldDisplay() const { return true; } -void ModFolderPage::retranslate() -{ - ui->retranslateUi(this); -} - bool CoreModFolderPage::shouldDisplay() const { - if (ModFolderPage::shouldDisplay()) - { - auto inst = dynamic_cast<MinecraftInstance *>(m_inst); + if (ModFolderPage::shouldDisplay()) { + auto inst = dynamic_cast<MinecraftInstance*>(m_instance); if (!inst) return true; + auto version = inst->getPackProfile(); + if (!version) return true; - if(!version->getComponent("net.minecraftforge")) - { + if (!version->getComponent("net.minecraftforge")) return false; - } - if(!version->getComponent("net.minecraft")) - { + if (!version->getComponent("net.minecraft")) return false; - } - if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) - { + if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) return true; - } + } return false; } -bool ModFolderPage::modListFilter(QKeyEvent *keyEvent) -{ - switch (keyEvent->key()) - { - case Qt::Key_Delete: - on_actionRemove_triggered(); - return true; - case Qt::Key_Plus: - on_actionAdd_triggered(); - return true; - default: - break; - } - return QWidget::eventFilter(ui->modTreeView, keyEvent); -} - -bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev) -{ - if (ev->type() != QEvent::KeyPress) - { - return QWidget::eventFilter(obj, ev); - } - QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev); - if (obj == ui->modTreeView) - return modListFilter(keyEvent); - return QWidget::eventFilter(obj, ev); -} - -void ModFolderPage::on_actionAdd_triggered() -{ - if(!m_controlsEnabled) { - return; - } - auto list = GuiUtil::BrowseForFiles( - m_helpName, - tr("Select %1", - "Select whatever type of files the page contains. Example: 'Loader Mods'") - .arg(m_displayName), - m_fileSelectionFilter.arg(m_displayName), APPLICATION->settings()->get("CentralModsDir").toString(), - this->parentWidget()); - if (!list.empty()) - { - for (auto filename : list) - { - m_mods->installMod(filename); - } - } -} - -void ModFolderPage::on_actionEnable_triggered() -{ - if(!m_controlsEnabled) { - return; - } - auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); - m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable); -} - -void ModFolderPage::on_actionDisable_triggered() +void ModFolderPage::installMods() { - if(!m_controlsEnabled) { + if (!m_controlsEnabled) return; - } - auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); - m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable); -} - -void ModFolderPage::on_actionRemove_triggered() -{ - if(!m_controlsEnabled) { + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile(); + if (profile->getModLoaders() == ModAPI::Unspecified) { + QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); return; } - auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); - m_mods->deleteMods(selection.indexes()); -} -void ModFolderPage::on_actionInstall_mods_triggered() -{ - if(!m_controlsEnabled) { - return; - } - if(m_inst->typeName() != "Minecraft"){ - return; //this is a null instance or a legacy instance - } - auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); - if (profile->getModLoader() == ModAPI::Unspecified) { - QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); - return; - } - ModDownloadDialog mdownload(m_mods, this, m_inst); + ModDownloadDialog mdownload(m_model, this, m_instance); if (mdownload.exec()) { - SequentialTask* tasks = new SequentialTask(this); + ConcurrentTask* tasks = new ConcurrentTask(this); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + tasks->deleteLater(); }); - for (auto task : mdownload.getTasks()) { + for (auto& task : mdownload.getTasks()) { tasks->addTask(task); } + ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); - m_mods->update(); - } -} - -void ModFolderPage::on_actionView_configs_triggered() -{ - DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true); -} - -void ModFolderPage::on_actionView_Folder_triggered() -{ - DesktopServices::openDirectory(m_mods->dir().absolutePath(), true); -} -void ModFolderPage::modCurrent(const QModelIndex ¤t, const QModelIndex &previous) -{ - if (!current.isValid()) - { - ui->frame->clear(); - return; + m_model->update(); } - auto sourceCurrent = m_filterModel->mapToSource(current); - int row = sourceCurrent.row(); - Mod &m = m_mods->operator[](row); - ui->frame->updateWithMod(m); } diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 72e2d404..19caa732 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,108 +36,31 @@ #pragma once -#include <QMainWindow> +#include "ExternalResourcesPage.h" -#include "minecraft/MinecraftInstance.h" -#include "ui/pages/BasePage.h" - -#include <Application.h> - -class ModFolderModel; -namespace Ui -{ -class ModFolderPage; -} - -class ModFolderPage : public QMainWindow, public BasePage -{ +class ModFolderPage : public ExternalResourcesPage { Q_OBJECT -public: - explicit ModFolderPage( - BaseInstance *inst, - std::shared_ptr<ModFolderModel> mods, - QString id, - QString iconName, - QString displayName, - QString helpPage = "", - QWidget *parent = 0 - ); - virtual ~ModFolderPage(); - - void setFilter(const QString & filter) - { - m_fileSelectionFilter = filter; - } + public: + explicit ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = nullptr); + virtual ~ModFolderPage() = default; - virtual QString displayName() const override - { - return m_displayName; - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon(m_iconName); - } - virtual QString id() const override - { - return m_id; - } - virtual QString helpPage() const override - { - return m_helpName; - } - virtual bool shouldDisplay() const override; - void retranslate() override; - - virtual void openedImpl() override; - virtual void closedImpl() override; -protected: - bool eventFilter(QObject *obj, QEvent *ev) override; - bool modListFilter(QKeyEvent *ev); - QMenu * createPopupMenu() override; + void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } -protected: - BaseInstance *m_inst = nullptr; + virtual QString displayName() const override { return tr("Mods"); } + virtual QIcon icon() const override { return APPLICATION->getThemedIcon("loadermods"); } + virtual QString id() const override { return "mods"; } + virtual QString helpPage() const override { return "Loader-mods"; } -protected: - Ui::ModFolderPage *ui = nullptr; - std::shared_ptr<ModFolderModel> m_mods; - QSortFilterProxyModel *m_filterModel = nullptr; - QString m_iconName; - QString m_id; - QString m_displayName; - QString m_helpName; - QString m_fileSelectionFilter; - QString m_viewFilter; - bool m_controlsEnabled = true; - -public -slots: - void modCurrent(const QModelIndex ¤t, const QModelIndex &previous); + virtual bool shouldDisplay() const override; -private -slots: - void modItemActivated(const QModelIndex &index); - void on_filterTextChanged(const QString & newContents); - void on_RunningState_changed(bool running); - void on_actionAdd_triggered(); - void on_actionRemove_triggered(); - void on_actionEnable_triggered(); - void on_actionDisable_triggered(); - void on_actionInstall_mods_triggered(); - void on_actionView_Folder_triggered(); - void on_actionView_configs_triggered(); - void ShowContextMenu(const QPoint &pos); + private slots: + void installMods(); }; -class CoreModFolderPage : public ModFolderPage -{ -public: - explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, QString id, - QString iconName, QString displayName, QString helpPage = "", - QWidget *parent = 0); - virtual ~CoreModFolderPage() - { - } +class CoreModFolderPage : public ModFolderPage { + public: + explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0); + virtual ~CoreModFolderPage() = default; virtual bool shouldDisplay() const; }; diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 8054926c..a6c9fdd3 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -35,24 +35,28 @@ #pragma once -#include "ModFolderPage.h" -#include "ui_ModFolderPage.h" +#include "ExternalResourcesPage.h" +#include "ui_ExternalResourcesPage.h" -class ResourcePackPage : public ModFolderPage +class ResourcePackPage : public ExternalResourcesPage { Q_OBJECT public: explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0) - : ModFolderPage(instance, instance->resourcePackList(), "resourcepacks", - "resourcepacks", tr("Resource packs"), "Resource-packs", parent) + : ExternalResourcesPage(instance, instance->resourcePackList(), parent) { - ui->actionView_configs->setVisible(false); + ui->actionViewConfigs->setVisible(false); } virtual ~ResourcePackPage() {} + QString displayName() const override { return tr("Resource packs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } + QString id() const override { return "resourcepacks"; } + QString helpPage() const override { return "Resource-packs"; } + virtual bool shouldDisplay() const override { - return !m_inst->traits().contains("no-texturepacks") && - !m_inst->traits().contains("texturepacks"); + return !m_instance->traits().contains("no-texturepacks") && + !m_instance->traits().contains("texturepacks"); } }; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 2cf17b32..c97253e4 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -49,6 +50,7 @@ #include <QClipboard> #include <QKeyEvent> #include <QMenu> +#include <QRegularExpression> #include <Application.h> @@ -153,7 +155,7 @@ public: if (role == Qt::DisplayRole || role == Qt::EditRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); - return result.toString().remove(QRegExp("\\.png$")); + return result.toString().remove(QRegularExpression("\\.png$")); } if (role == Qt::DecorationRole) { @@ -269,7 +271,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent) ui->listView->setViewMode(QListView::IconMode); ui->listView->setResizeMode(QListView::Adjust); ui->listView->installEventFilter(this); - ui->listView->setEditTriggers(0); + ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 2af6164c..e5cce96c 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -287,7 +288,11 @@ public: return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); +#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) + m_servers.swapItemsAt(row-1, row); +#else m_servers.swap(row-1, row); +#endif endMoveRows(); scheduleSave(); return true; @@ -305,7 +310,11 @@ public: return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); +#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) + m_servers.swapItemsAt(row+1, row); +#else m_servers.swap(row+1, row); +#endif endMoveRows(); scheduleSave(); return true; @@ -614,7 +623,7 @@ ServersPage::ServersPage(InstancePtr inst, QWidget* parent) auto selectionModel = ui->serversView->selectionModel(); connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); - connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); + connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::runningStateChanged); connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); @@ -654,7 +663,7 @@ QMenu * ServersPage::createPopupMenu() return filteredMenu; } -void ServersPage::on_RunningState_changed(bool running) +void ServersPage::runningStateChanged(bool running) { if(m_locked == running) { diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 5173712c..37399d49 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -97,7 +98,7 @@ private slots: void on_actionMove_Down_triggered(); void on_actionJoin_triggered(); - void on_RunningState_changed(bool running); + void runningStateChanged(bool running); void nameEdited(const QString & name); void addressEdited(const QString & address); diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 7d4f5074..2cc056c8 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -35,21 +35,25 @@ #pragma once -#include "ModFolderPage.h" -#include "ui_ModFolderPage.h" +#include "ExternalResourcesPage.h" +#include "ui_ExternalResourcesPage.h" -class ShaderPackPage : public ModFolderPage +class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT public: explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0) - : ModFolderPage(instance, instance->shaderPackList(), "shaderpacks", - "shaderpacks", tr("Shader packs"), "Resource-packs", parent) + : ExternalResourcesPage(instance, instance->shaderPackList(), parent) { - ui->actionView_configs->setVisible(false); + ui->actionViewConfigs->setVisible(false); } virtual ~ShaderPackPage() {} + QString displayName() const override { return tr("Shader packs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } + QString id() const override { return "shaderpacks"; } + QString helpPage() const override { return "Resource-packs"; } + virtual bool shouldDisplay() const override { return true; diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index e8cefe6e..f550a5bc 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -35,23 +35,27 @@ #pragma once -#include "ModFolderPage.h" -#include "ui_ModFolderPage.h" +#include "ExternalResourcesPage.h" +#include "ui_ExternalResourcesPage.h" -class TexturePackPage : public ModFolderPage +class TexturePackPage : public ExternalResourcesPage { Q_OBJECT public: explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0) - : ModFolderPage(instance, instance->texturePackList(), "texturepacks", "resourcepacks", - tr("Texture packs"), "Texture-packs", parent) + : ExternalResourcesPage(instance, instance->texturePackList(), parent) { - ui->actionView_configs->setVisible(false); + ui->actionViewConfigs->setVisible(false); } virtual ~TexturePackPage() {} + QString displayName() const override { return tr("Texture packs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } + QString id() const override { return "texturepacks"; } + QString helpPage() const override { return "Texture-packs"; } + virtual bool shouldDisplay() const override { - return m_inst->traits().contains("texturepacks"); + return m_instance->traits().contains("texturepacks"); } }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 23e2367b..468ff35c 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -2,7 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 76725539..647b04a7 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c7bc13d8..30196aad 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -2,7 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -110,14 +110,16 @@ void ImportPage::updateState() { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); - // mrpack is a modrinth pack // Allow non-latin people to use ZIP files! - auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); - if(fi.exists() && (zip || fi.suffix() == "mrpack")) + bool isZip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); + // mrpack is a modrinth pack + bool isMRPack = fi.suffix() == "mrpack"; + + if(fi.exists() && (isZip || isMRPack)) { QFileInfo fi(url.fileName()); - dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); dialog->setSuggestedIcon("default"); } } @@ -130,7 +132,7 @@ void ImportPage::updateState() } // hook, line and sinker. QFileInfo fi(url.fileName()); - dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); dialog->setSuggestedIcon("default"); } } @@ -149,7 +151,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); - filter += ";;" + tr("Modrinth pack (*.mrpack)"); + //: Option for filtering for *.mrpack files when importing + filter += ";;" + tr("Modrinth pack") + " (*.mrpack)"; const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 540ee2fd..94b1f099 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -38,27 +38,48 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant } ModPlatform::IndexedPack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::ToolTipRole) { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); - return edit; + switch (role) { + case Qt::DisplayRole: { + return pack.name; } - return pack.description; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.logoName)) { + auto icon = m_logoMap.value(pack.logoName); + // FIXME: This doesn't really belong here, but Qt doesn't offer a good way right now ;( + auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48)); + + return icon_scaled; + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + // un-const-ify this + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::FontRole: { + QFont font; + if (m_parent->getDialog()->isModSelected(pack.name)) { + font.setBold(true); + font.setUnderline(true); + } + + return font; + } + default: + break; } return {}; @@ -68,7 +89,7 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() }); } void ListModel::performPaginatedSearch() @@ -76,7 +97,12 @@ void ListModel::performPaginatedSearch() auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); m_parent->apiProvider()->searchMods( - this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); +} + +void ListModel::requestModInfo(ModPlatform::IndexedPack& current) +{ + m_parent->apiProvider()->getModInfo(this, current); } void ListModel::refresh() @@ -193,6 +219,10 @@ void ListModel::searchRequestFinished(QJsonDocument& doc) searchState = CanPossiblyFetchMore; } + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (newList.size() == 0) + return; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); modpacks.append(newList); endInsertRows(); @@ -225,6 +255,21 @@ void ListModel::searchRequestFailed(QString reason) } } +void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack) +{ + qDebug() << "Loading mod info"; + + try { + auto obj = Json::requireObject(doc); + loadExtraPackInfo(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); + } + + m_parent->updateUi(); +} + void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { auto& current = m_parent->getCurrent(); diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index d460cff2..dd22407c 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -36,9 +36,11 @@ class ListModel : public QAbstractListModel { void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, const int sort, const bool filter_changed); + void requestModInfo(ModPlatform::IndexedPack& current); void requestModVersions(const ModPlatform::IndexedPack& current); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); @@ -49,6 +51,8 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc); void searchRequestFailed(QString reason); + void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack); + void versionRequestSucceeded(QJsonDocument doc, QString addonId); protected slots: diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 6dd3a453..200fe59e 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,4 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ModPage.h" +#include "Application.h" #include "ui_ModPage.h" #include <QKeyEvent> @@ -94,28 +130,6 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!first.isValid()) { return; } current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - - if (!current.authors.empty()) { - auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { - if (author.url.isEmpty()) { return author.name; } - return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "<br>" + tr(" by ") + authorStrs.join(", "); - } - text += "<br><br>"; - - ui->packDescription->setHtml(text + current.description); if (!current.versionsLoaded) { qDebug() << QString("Loading %1 mod versions").arg(debugName()); @@ -132,6 +146,13 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) updateSelectionButton(); } + + if(!current.extraDataLoaded){ + qDebug() << QString("Loading %1 mod info").arg(debugName()); + listModel->requestModInfo(current); + } + + updateUi(); } void ModPage::onVersionSelectionChanged(QString data) @@ -150,7 +171,8 @@ void ModPage::onModSelected() if (dialog->isModSelected(current.name, version.fileName)) { dialog->removeSelectedMod(current.name); } else { - dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed)); } updateSelectionButton(); @@ -175,7 +197,7 @@ void ModPage::updateModVersions(int prev_count) bool valid = false; for(auto& mcVer : m_filter->versions){ //NOTE: Flame doesn't care about loader, so passing it changes nothing. - if (validateVersion(version, mcVer.toString(), packProfile->getModLoader())) { + if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) { valid = true; break; } @@ -207,3 +229,61 @@ void ModPage::updateSelectionButton() ui->modSelectionButton->setText(tr("Deselect mod for download")); } } + +void ModPage::updateUi() +{ + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + + if (!current.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { return author.name; } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + + + if(current.extraDataLoaded) { + if (!current.extraData.donate.isEmpty()) { + text += "<br><br>" + tr("Donate information: "); + auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { + return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current.extraData.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + if (!current.extraData.issuesUrl.isEmpty() + || !current.extraData.sourceUrl.isEmpty() + || !current.extraData.wikiUrl.isEmpty() + || !current.extraData.discordUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current.extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>"; + if (!current.extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>"; + if (!current.extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>"; + if (!current.extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>"; + } + + text += "<hr>"; + + ui->packDescription->setHtml(text + current.description); +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index eb89b0e2..cf00e16e 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -36,11 +36,14 @@ class ModPage : public QWidget, public BasePage { void retranslate() override; + void updateUi(); + auto shouldDisplay() const -> bool override = 0; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; - auto apiProvider() const -> const ModAPI* { return api.get(); }; + auto apiProvider() -> ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; } + auto getDialog() const -> const ModDownloadDialog* { return dialog; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(int prev_count = -1); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 26aa60af..004fdc57 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -43,8 +43,11 @@ #include "modplatform/atlauncher/ATLShareCode.h" #include "Application.h" -AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods) - : QAbstractListModel(parent), m_mods(mods) { +AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) + : QAbstractListModel(parent) + , m_version(version) + , m_mods(mods) +{ // fill mod index for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); @@ -97,6 +100,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; @@ -223,7 +231,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<br><br>%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) { @@ -287,12 +309,13 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool } } - -AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods) - : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { +AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> 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 953b288e..8e02444e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -56,7 +56,7 @@ public: DescriptionColumn, }; - AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods); + AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods); QVector<QString> getResult(); @@ -86,7 +86,9 @@ private: NetJob::Ptr m_jobPtr; QByteArray m_response; + ATLauncher::PackVersion m_version; QVector<ATLauncher::VersionMod> m_mods; + QMap<QString, bool> m_selection; QMap<QString, int> m_index; QMap<QString, QVector<QString>> m_dependants; @@ -96,7 +98,7 @@ class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods); + AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods); ~AtlOptionalModDialog() override; QVector<QString> getResult() { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index df9b9207..8de5211c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -45,8 +45,12 @@ #include <BuildConfig.h> -AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +#include <QMessageBox> + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent) + , ui(new Ui::AtlPage) + , dialog(dialog) { ui->setupUi(this); @@ -113,7 +117,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) @@ -169,8 +173,9 @@ void AtlPage::onVersionSelectionChanged(QString data) suggestCurrent(); } -QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) { - AtlOptionalModDialog optionalModDialog(this, mods); +QVector<QString> AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) +{ + AtlOptionalModDialog optionalModDialog(this, version, mods); optionalModDialog.exec(); return optionalModDialog.getResult(); } @@ -210,3 +215,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 c95b0127..aa6d5da1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -84,7 +84,8 @@ private: void suggestCurrent(); QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; - QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override; + QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override; + void displayMessage(QString message) override; private slots: void triggerSearch(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 70759994..10d34218 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,9 +61,9 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { - Q_UNUSED(loader); + Q_UNUSED(loaders); return ver.mcVersion.contains(mineVer); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 27cbdb8c..445d0368 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,7 +55,7 @@ class FlameModPage : public ModPage { inline auto debugName() const -> QString override { return "Flame"; } inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index f97536e8..b9804681 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -57,6 +57,17 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return QVariant(); } +bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + return false; + + modpacks[pos] = value.value<Flame::IndexedPack>(); + + return true; +} + void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); @@ -210,6 +221,11 @@ void Flame::ListModel::searchRequestFinished() nextSearchOffset += 25; searchState = CanPossiblyFetchMore; } + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (newList.size() == 0) + return; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); modpacks.append(newList); endInsertRows(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index 536f6add..cab666cc 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -34,6 +34,7 @@ public: int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool canFetchMore(const QModelIndex & parent) const override; void fetchMore(const QModelIndex & parent) override; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index ec774621..7d2ba2e2 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -107,41 +107,18 @@ void FlamePage::triggerSearch() listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); } -void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) +void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) { ui->versionSelectionBox->clear(); - if (!first.isValid()) { + if (!curr.isValid()) { if (isOpened) { dialog->setSuggestedPack(); } return; } - current = listModel->data(first, Qt::UserRole).value<Flame::IndexedPack>(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor& author) { - if (author.url.isEmpty()) { - return author.name; - } - return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "<br>" + tr(" by ") + authorStrs.join(", "); - } - text += "<br><br>"; - - ui->packDescription->setHtml(text + current.description); + current = listModel->data(curr, Qt::UserRole).value<Flame::IndexedPack>(); if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; @@ -150,7 +127,7 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) int addonId = current.addonId; netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { if (addonId != current.addonId) { return; // wrong request } @@ -174,6 +151,16 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } + QVariant current_updated; + current_updated.setValue(current); + + if (!listModel->setData(curr, current_updated, Qt::UserRole)) + qWarning() << "Failed to cache versions for the current pack!"; + + // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. + if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) { + ui->versionSelectionBox->addItem(tr("No version is available!"), -1); + } suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { @@ -188,6 +175,13 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); } + + // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. + if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) { + ui->versionSelectionBox->addItem(tr("No version is available!"), -1); + } + + updateUi(); } void FlamePage::suggestCurrent() @@ -196,12 +190,12 @@ void FlamePage::suggestCurrent() return; } - if (selectedVersion.isEmpty()) { + if (selectedVersion.isEmpty() || selectedVersion == "-1") { dialog->setSuggestedPack(); return; } - dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); + dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this)); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); listModel->getLogo(current.logoName, current.logoUrl, @@ -217,3 +211,46 @@ void FlamePage::onVersionSelectionChanged(QString data) selectedVersion = ui->versionSelectionBox->currentData().toString(); suggestCurrent(); } + +void FlamePage::updateUi() +{ + QString text = ""; + QString name = current.name; + + if (current.extra.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.extra.websiteUrl + "\">" + name + "</a>"; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor& author) { + if (author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + + if(current.extraInfoLoaded) { + if (!current.extra.issuesUrl.isEmpty() + || !current.extra.sourceUrl.isEmpty() + || !current.extra.wikiUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current.extra.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>"; + if (!current.extra.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>"; + if (!current.extra.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>"; + } + + text += "<hr>"; + + ui->packDescription->setHtml(text + current.description); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index baac57c9..8130e416 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -79,6 +79,8 @@ public: virtual bool shouldDisplay() const override; void retranslate() override; + void updateUi(); + void openedImpl() override; bool eventFilter(QObject * watched, QEvent * event) override; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 6d8d8e10..aab16421 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -1,90 +1,110 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>FlamePage</class> - <widget class="QWidget" name="FlamePage"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>837</width> - <height>685</height> - </rect> - </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="1" column="0" colspan="2"> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="1" column="0"> - <widget class="QListView" name="packView"> - <property name="iconSize"> - <size> - <width>48</width> - <height>48</height> - </size> - </property> - <property name="horizontalScrollBarPolicy"> - <enum>Qt::ScrollBarAlwaysOff</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QTextBrowser" name="packDescription"> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - <property name="openLinks"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - <item row="2" column="0" colspan="2"> - <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> - <item row="0" column="2"> - <widget class="QComboBox" name="versionSelectionBox"/> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Version selected:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QComboBox" name="sortByBox"/> - </item> - </layout> - </item> - <item row="0" column="1"> - <widget class="QPushButton" name="searchButton"> - <property name="text"> - <string>Search</string> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLineEdit" name="searchEdit"> - <property name="placeholderText"> - <string>Search and filter...</string> - </property> - </widget> - </item> + <class>FlamePage</class> + <widget class="QWidget" name="FlamePage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Note: CurseForge allows creators to block access to third-party tools like PolyMC. As such, you may need to manually download some mods to be able to install a modpack.</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <layout class="QHBoxLayout"> + <item> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> </layout> - </widget> - <tabstops> - <tabstop>searchEdit</tabstop> - <tabstop>searchButton</tabstop> - <tabstop>packView</tabstop> - <tabstop>packDescription</tabstop> - <tabstop>sortByBox</tabstop> - <tabstop>versionSelectionBox</tabstop> - </tabstops> - <resources/> - <connections/> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout"> + <item> + <widget class="QListView" name="packView"> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="3" column="0"> + <layout class="QHBoxLayout"> + <item> + <widget class="QComboBox" name="sortByBox"/> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> </ui> diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index 37244fed..ad15b6e6 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -122,10 +122,10 @@ void ListModel::requestFinished() jobPtr.reset(); remainingPacks.clear(); - QJsonParseError parse_error; + QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -169,7 +169,7 @@ void ListModel::packRequestFinished() QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -184,7 +184,7 @@ void ListModel::packRequestFinished() catch (const JSONValidationError &e) { qDebug() << QString::fromUtf8(response); - qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause(); return; } @@ -192,7 +192,7 @@ void ListModel::packRequestFinished() // ignore those "dud" packs. if (pack.versions.empty()) { - qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions"; + qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions"; } else { @@ -270,7 +270,7 @@ void ListModel::requestLogo(QString logo, QString url) bool stale = entry->isStale(); - NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo), APPLICATION->network()); + NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 63b944c4..2d135e59 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ListModel.h" #include "Application.h" @@ -133,7 +168,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const ((ListModel *)this)->requestLogo(pack.logo); return icon; } - else if(role == Qt::TextColorRole) + else if(role == Qt::ForegroundRole) { if(pack.broken) { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 27a12cda..6ffbd312 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -151,7 +152,7 @@ void Page::openedImpl() ftbFetchTask->fetch(); ftbPrivatePacks->load(); - ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList()); + ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().values()); initialized = true; } suggestCurrent(); @@ -175,7 +176,7 @@ void Page::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); QString editedLogoName; if(selected.logo.toLower().startsWith("ftb")) { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index 15e5d432..f4231d8d 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -25,7 +25,7 @@ <widget class="QTreeView" name="publicPackList"> <property name="maximumSize"> <size> - <width>250</width> + <width>16777215</width> <height>16777215</height> </size> </property> @@ -51,7 +51,7 @@ <widget class="QTreeView" name="thirdPartyPackList"> <property name="maximumSize"> <size> - <width>250</width> + <width>16777215</width> <height>16777215</height> </size> </property> @@ -71,7 +71,7 @@ <widget class="QTreeView" name="privatePackList"> <property name="maximumSize"> <size> - <width>250</width> + <width>16777215</width> <height>16777215</height> </size> </property> diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp index 1d9f4d60..af92e63e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp @@ -30,6 +30,11 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) Modrinth::loadIndexedPack(m, obj); } +void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + Modrinth::loadExtraPackData(m, obj); +} + void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index ae7b0bdd..386897fd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -31,6 +31,7 @@ class ListModel : public ModPlatform::ListModel { private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index d3a1f859..5fa00b9b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,9 +61,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); } -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader); + auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders); auto loaderCompatible = false; for (auto remoteLoader : ver.loaders) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index b1e72bfe..94985f63 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,7 +55,7 @@ class ModrinthModPage : public ModPage { inline auto debugName() const -> QString override { return "Modrinth"; } inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 7cacf37a..3633d575 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -86,6 +87,7 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian } else if (role == Qt::DecorationRole) { if (m_logoMap.contains(pack.iconName)) { auto icon = m_logoMap.value(pack.iconName); + // FIXME: This doesn't really belong here, but Qt doesn't offer a good way right now ;( auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48)); return icon_scaled; @@ -102,6 +104,17 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return {}; } +bool ModpackListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + return false; + + modpacks[pos] = value.value<Modrinth::Modpack>(); + + return true; +} + void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API @@ -159,15 +172,15 @@ static auto sortFromIndex(int index) -> QString { switch(index){ default: - case 1: + case 0: return "relevance"; - case 2: + case 1: return "downloads"; - case 3: + case 2: return "follows"; - case 4: + case 3: return "newest"; - case 5: + case 4: return "updated"; } @@ -277,6 +290,10 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) searchState = CanPossiblyFetchMore; } + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (newList.size() == 0) + return; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); modpacks.append(newList); endInsertRows(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 14aa6747..6f33e11e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -63,6 +63,7 @@ class ModpackListModel : public QAbstractListModel { /* Retrieve information from the model at a given index with the given role */ auto data(const QModelIndex& index, int role) const -> QVariant override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 9bd24b57..df29c0c3 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -101,18 +101,18 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } -void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) +void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) { ui->versionSelectionBox->clear(); - if (!first.isValid()) { + if (!curr.isValid()) { if (isOpened) { dialog->setSuggestedPack(); } return; } - current = m_model->data(first, Qt::UserRole).value<Modrinth::Modpack>(); + current = m_model->data(curr, Qt::UserRole).value<Modrinth::Modpack>(); auto name = current.name; if (!current.extraInfoLoaded) { @@ -125,7 +125,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { if (id != current.id) { return; // wrong request? } @@ -149,6 +149,13 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) } updateUI(); + + QVariant current_updated; + current_updated.setValue(current); + + if (!m_model->setData(curr, current_updated, Qt::UserRole)) + qWarning() << "Failed to cache extra info for the current pack!"; + suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { @@ -170,7 +177,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) netJob->addNetAction( Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { if (id != current.id) { return; // wrong request? } @@ -192,9 +199,18 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) } for (auto version : current.versions) { - ui->versionSelectionBox->addItem(version.version, QVariant(version.id)); + if (!version.name.contains(version.version)) + ui->versionSelectionBox->addItem(QString("%1 — %2").arg(version.name, version.version), QVariant(version.id)); + else + ui->versionSelectionBox->addItem(version.name, QVariant(version.id)); } + QVariant current_updated; + current_updated.setValue(current); + + if (!m_model->setData(curr, current_updated, Qt::UserRole)) + qWarning() << "Failed to cache versions for the current pack!"; + suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { @@ -205,7 +221,10 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) } else { for (auto version : current.versions) { - ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id)); + if (!version.name.contains(version.version)) + ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id)); + else + ui->versionSelectionBox->addItem(version.name, QVariant(version.id)); } suggestCurrent(); @@ -224,7 +243,37 @@ void ModrinthPage::updateUI() // TODO: Implement multiple authors with links text += "<br>" + tr(" by ") + QString("<a href=%1>%2</a>").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); - text += "<br>"; + if (current.extraInfoLoaded) { + if (!current.extra.donate.isEmpty()) { + text += "<br><br>" + tr("Donate information: "); + auto donateToStr = [](Modrinth::DonationData& donate) -> QString { + return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current.extra.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + if (!current.extra.issuesUrl.isEmpty() + || !current.extra.sourceUrl.isEmpty() + || !current.extra.wikiUrl.isEmpty() + || !current.extra.discordUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current.extra.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>"; + if (!current.extra.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>"; + if (!current.extra.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>"; + if (!current.extra.discordUrl.isEmpty()) + text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extra.discordUrl) + "<br>"; + } + + text += "<hr>"; HoeDown h; text += h.process(current.extra.body.toUtf8()); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 4fb59cdf..6a34701d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>837</width> - <height>685</height> + <width>800</width> + <height>600</height> </rect> </property> <layout class="QVBoxLayout"> @@ -24,6 +24,9 @@ <property name="alignment"> <set>Qt::AlignCenter</set> </property> + <property name="wordWrap"> + <bool>true</bool> + </property> </widget> </item> <item> @@ -60,9 +63,6 @@ <height>48</height> </size> </property> - <property name="uniformItemSizes"> - <bool>true</bool> - </property> </widget> </item> <item> diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 9c9d1e75..742f4f2a 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -217,6 +217,11 @@ void Technic::ListModel::searchRequestFinished() return; } searchState = Finished; + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (newList.size() == 0) + return; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); modpacks.append(newList); endInsertRows(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index ca6a9b7e..15bf645f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -60,7 +60,11 @@ </widget> </item> <item row="0" column="1"> - <widget class="QTextBrowser" name="packDescription"/> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> </item> </layout> </item> diff --git a/launcher/ui/setupwizard/PasteWizardPage.cpp b/launcher/ui/setupwizard/PasteWizardPage.cpp new file mode 100644 index 00000000..0f47da4b --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.cpp @@ -0,0 +1,42 @@ +#include "PasteWizardPage.h" +#include "ui_PasteWizardPage.h" + +#include "Application.h" +#include "net/PasteUpload.h" + +PasteWizardPage::PasteWizardPage(QWidget *parent) : + BaseWizardPage(parent), + ui(new Ui::PasteWizardPage) +{ + ui->setupUi(this); +} + +PasteWizardPage::~PasteWizardPage() +{ + delete ui; +} + +void PasteWizardPage::initializePage() +{ +} + +bool PasteWizardPage::validatePage() +{ + auto s = APPLICATION->settings(); + QString prevPasteURL = s->get("PastebinURL").toString(); + s->reset("PastebinURL"); + if (ui->previousSettingsRadioButton->isChecked()) + { + bool usingDefaultBase = prevPasteURL == PasteUpload::PasteTypes.at(PasteUpload::PasteType::NullPointer).defaultBase; + s->set("PastebinType", PasteUpload::PasteType::NullPointer); + if (!usingDefaultBase) + s->set("PastebinCustomAPIBase", prevPasteURL); + } + + return true; +} + +void PasteWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/PasteWizardPage.h b/launcher/ui/setupwizard/PasteWizardPage.h new file mode 100644 index 00000000..513a14cb --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.h @@ -0,0 +1,27 @@ +#ifndef PASTEDEFAULTSCONFIRMATIONWIZARD_H +#define PASTEDEFAULTSCONFIRMATIONWIZARD_H + +#include <QWidget> +#include "BaseWizardPage.h" + +namespace Ui { +class PasteWizardPage; +} + +class PasteWizardPage : public BaseWizardPage +{ + Q_OBJECT + +public: + explicit PasteWizardPage(QWidget *parent = nullptr); + ~PasteWizardPage(); + + void initializePage() override; + bool validatePage() override; + void retranslate() override; + +private: + Ui::PasteWizardPage *ui; +}; + +#endif // PASTEDEFAULTSCONFIRMATIONWIZARD_H diff --git a/launcher/ui/setupwizard/PasteWizardPage.ui b/launcher/ui/setupwizard/PasteWizardPage.ui new file mode 100644 index 00000000..247d3a75 --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.ui @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PasteWizardPage</class> + <widget class="QWidget" name="PasteWizardPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>The default paste service has changed to mclo.gs, please choose what you want to do with your settings.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="defaultSettingsRadioButton"> + <property name="text"> + <string>Use new default service</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="previousSettingsRadioButton"> + <property name="text"> + <string>Keep previous settings</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup</string> + </attribute> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>156</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> + <buttongroups> + <buttongroup name="buttonGroup"/> + </buttongroups> +</ui> diff --git a/launcher/ui/widgets/CustomCommands.cpp b/launcher/ui/widgets/CustomCommands.cpp index 5a718b54..5ab90395 100644 --- a/launcher/ui/widgets/CustomCommands.cpp +++ b/launcher/ui/widgets/CustomCommands.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/widgets/CustomCommands.h b/launcher/ui/widgets/CustomCommands.h index 4a7a17ef..ed10ba95 100644 --- a/launcher/ui/widgets/CustomCommands.h +++ b/launcher/ui/widgets/CustomCommands.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 340518b1..f0765909 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -11,6 +11,7 @@ #include <sys.h> +#include "JavaCommon.h" #include "java/JavaInstall.h" #include "java/JavaUtils.h" #include "FileSystem.h" @@ -133,6 +134,10 @@ void JavaSettingsWidget::initialize() void JavaSettingsWidget::refresh() { + if (JavaUtils::getJavaCheckPath().isEmpty()) { + JavaCommon::javaCheckNotFound(this); + return; + } m_versionWidget->loadList(); } diff --git a/launcher/ui/widgets/LabeledToolButton.cpp b/launcher/ui/widgets/LabeledToolButton.cpp index ab2d3278..f52e49c9 100644 --- a/launcher/ui/widgets/LabeledToolButton.cpp +++ b/launcher/ui/widgets/LabeledToolButton.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * 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 <QLabel> @@ -80,9 +100,7 @@ QSize LabeledToolButton::sizeHint() const if (popupMode() == MenuButtonPopup) w += style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, this); - QSize rawSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this); - QSize sizeHint = rawSize.expandedTo(QApplication::globalStrut()); - return sizeHint; + return style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this); } diff --git a/launcher/ui/widgets/LogView.cpp b/launcher/ui/widgets/LogView.cpp index 26a2a527..9c46438d 100644 --- a/launcher/ui/widgets/LogView.cpp +++ b/launcher/ui/widgets/LogView.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "LogView.h" #include <QTextBlock> #include <QScrollBar> @@ -102,7 +137,7 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) { format.setFont(font.value<QFont>()); } - auto fg = m_model->data(idx, Qt::TextColorRole); + auto fg = m_model->data(idx, Qt::ForegroundRole); if(fg.isValid()) { format.setForeground(fg.value<QColor>()); diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp index 8c4bd690..7d78006b 100644 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ b/launcher/ui/widgets/MCModInfoFrame.cpp @@ -32,7 +32,7 @@ void MCModInfoFrame::updateWithMod(Mod &m) QString text = ""; QString name = ""; if (m.name().isEmpty()) - name = m.mmc_id(); + name = m.internal_id(); else name = m.name(); diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 2af7d731..419ccb66 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -66,7 +66,7 @@ public: protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - const QString pattern = filterRegExp().pattern(); + const QString pattern = filterRegularExpression().pattern(); const auto model = static_cast<PageModel *>(sourceModel()); const auto page = model->pages().at(sourceRow); if (!page->shouldDisplay()) @@ -171,7 +171,7 @@ void PageContainer::createUI() headerHLayout->addSpacerItem(new QSpacerItem(rightMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); headerHLayout->setContentsMargins(0, 6, 0, 0); - m_pageStack->setMargin(0); + m_pageStack->setContentsMargins(0, 0, 0, 0); m_pageStack->addWidget(new QWidget(this)); m_layout = new QGridLayout; diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp index aba0b1a1..0e126c65 100644 --- a/launcher/ui/widgets/VersionListView.cpp +++ b/launcher/ui/widgets/VersionListView.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include <QHeaderView> @@ -136,7 +156,7 @@ void VersionListView::paintInfoLabel(QPaintEvent *event) const auto innerBounds = bounds; innerBounds.adjust(10, 10, -10, -10); - QColor background = QApplication::palette().color(QPalette::Foreground); + QColor background = QApplication::palette().color(QPalette::WindowText); QColor foreground = QApplication::palette().color(QPalette::Base); foreground.setAlpha(190); painter.setFont(font); diff --git a/launcher/updater/DownloadTask_test.cpp b/launcher/updater/DownloadTask_test.cpp deleted file mode 100644 index 8e823a63..00000000 --- a/launcher/updater/DownloadTask_test.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include <QTest> -#include <QSignalSpy> - -#include "TestUtil.h" - -#include "updater/GoUpdate.h" -#include "updater/DownloadTask.h" -#include "updater/UpdateChecker.h" -#include <FileSystem.h> - -using namespace GoUpdate; - -FileSourceList encodeBaseFile(const char *suffix) -{ - auto base = QDir::currentPath(); - QUrl localFile = QUrl::fromLocalFile(base + suffix); - QString localUrlString = localFile.toString(QUrl::FullyEncoded); - auto item = FileSource("http", localUrlString); - return FileSourceList({item}); -} - -Q_DECLARE_METATYPE(VersionFileList) -Q_DECLARE_METATYPE(Operation) - -QDebug operator<<(QDebug dbg, const FileSource &f) -{ - dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url - << " comp=" << f.compressionType << ")"; - return dbg.maybeSpace(); -} - -QDebug operator<<(QDebug dbg, const VersionFileEntry &v) -{ - dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode - << " md5=" << v.md5 << " sources=" << v.sources << ")"; - return dbg.maybeSpace(); -} - -QDebug operator<<(QDebug dbg, const Operation::Type &t) -{ - switch (t) - { - case Operation::OP_REPLACE: - dbg << "OP_COPY"; - break; - case Operation::OP_DELETE: - dbg << "OP_DELETE"; - break; - } - return dbg.maybeSpace(); -} - -QDebug operator<<(QDebug dbg, const Operation &u) -{ - dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source - << " dest=" << u.destination << " mode=" << u.destinationMode << ")"; - return dbg.maybeSpace(); -} - -class DownloadTaskTest : public QObject -{ - Q_OBJECT -private -slots: - void initTestCase() - { - } - void cleanupTestCase() - { - } - - void test_parseVersionInfo_data() - { - QTest::addColumn<QByteArray>("data"); - QTest::addColumn<VersionFileList>("list"); - QTest::addColumn<QString>("error"); - QTest::addColumn<bool>("ret"); - - QTest::newRow("one") - << GET_TEST_FILE("data/1.json") - << (VersionFileList() - << VersionFileEntry{"fileOne", - 493, - encodeBaseFile("/data/fileOneA"), - "9eb84090956c484e32cb6c08455a667b"} - << VersionFileEntry{"fileTwo", - 644, - encodeBaseFile("/data/fileTwo"), - "38f94f54fa3eb72b0ea836538c10b043"} - << VersionFileEntry{"fileThree", - 750, - encodeBaseFile("/data/fileThree"), - "f12df554b21e320be6471d7154130e70"}) - << QString() << true; - QTest::newRow("two") - << GET_TEST_FILE("data/2.json") - << (VersionFileList() - << VersionFileEntry{"fileOne", - 493, - encodeBaseFile("/data/fileOneB"), - "42915a71277c9016668cce7b82c6b577"} - << VersionFileEntry{"fileTwo", - 644, - encodeBaseFile("/data/fileTwo"), - "38f94f54fa3eb72b0ea836538c10b043"}) - << QString() << true; - } - void test_parseVersionInfo() - { - QFETCH(QByteArray, data); - QFETCH(VersionFileList, list); - QFETCH(QString, error); - QFETCH(bool, ret); - - VersionFileList outList; - QString outError; - bool outRet = parseVersionInfo(data, outList, outError); - QCOMPARE(outRet, ret); - QCOMPARE(outList, list); - QCOMPARE(outError, error); - } - - void test_processFileLists_data() - { - QTest::addColumn<QString>("tempFolder"); - QTest::addColumn<VersionFileList>("currentVersion"); - QTest::addColumn<VersionFileList>("newVersion"); - QTest::addColumn<OperationList>("expectedOperations"); - - QTemporaryDir tempFolderObj; - QString tempFolder = tempFolderObj.path(); - // update fileOne, keep fileTwo, remove fileThree - QTest::newRow("test 1") - << tempFolder << (VersionFileList() - << VersionFileEntry{ - "data/fileOne", 493, - FileSourceList() - << FileSource( - "http", "http://host/path/fileOne-1"), - "9eb84090956c484e32cb6c08455a667b"} - << VersionFileEntry{ - "data/fileTwo", 644, - FileSourceList() - << FileSource( - "http", "http://host/path/fileTwo-1"), - "38f94f54fa3eb72b0ea836538c10b043"} - << VersionFileEntry{ - "data/fileThree", 420, - FileSourceList() - << FileSource( - "http", "http://host/path/fileThree-1"), - "f12df554b21e320be6471d7154130e70"}) - << (VersionFileList() - << VersionFileEntry{ - "data/fileOne", 493, - FileSourceList() - << FileSource("http", - "http://host/path/fileOne-2"), - "42915a71277c9016668cce7b82c6b577"} - << VersionFileEntry{ - "data/fileTwo", 644, - FileSourceList() - << FileSource("http", - "http://host/path/fileTwo-2"), - "38f94f54fa3eb72b0ea836538c10b043"}) - << (OperationList() - << Operation::DeleteOp("data/fileThree") - << Operation::CopyOp( - FS::PathCombine(tempFolder, - QString("data/fileOne").replace("/", "_")), - "data/fileOne", 493)); - } - void test_processFileLists() - { - QFETCH(QString, tempFolder); - QFETCH(VersionFileList, currentVersion); - QFETCH(VersionFileList, newVersion); - QFETCH(OperationList, expectedOperations); - - OperationList operations; - - shared_qobject_ptr<QNetworkAccessManager> network = new QNetworkAccessManager(); - processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy", network), operations); - qDebug() << (operations == expectedOperations); - qDebug() << operations; - qDebug() << expectedOperations; - QCOMPARE(operations, expectedOperations); - } -}; - -extern "C" -{ - QTEST_GUILESS_MAIN(DownloadTaskTest) -} - -#include "DownloadTask_test.moc" diff --git a/launcher/updater/UpdateChecker_test.cpp b/launcher/updater/UpdateChecker_test.cpp deleted file mode 100644 index ec55a40e..00000000 --- a/launcher/updater/UpdateChecker_test.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include <QTest> -#include <QSignalSpy> - -#include "TestUtil.h" -#include "updater/UpdateChecker.h" - -Q_DECLARE_METATYPE(UpdateChecker::ChannelListEntry) - -bool operator==(const UpdateChecker::ChannelListEntry &e1, const UpdateChecker::ChannelListEntry &e2) -{ - qDebug() << e1.url << "vs" << e2.url; - return e1.id == e2.id && - e1.name == e2.name && - e1.description == e2.description && - e1.url == e2.url; -} - -QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c) -{ - dbg.nospace() << "ChannelListEntry(id=" << c.id << " name=" << c.name << " description=" << c.description << " url=" << c.url << ")"; - return dbg.maybeSpace(); -} - -QString findTestDataUrl(const char *file) -{ - return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); -} - -class UpdateCheckerTest : public QObject -{ - Q_OBJECT -private -slots: - void initTestCase() - { - - } - void cleanupTestCase() - { - - } - - void tst_ChannelListParsing_data() - { - QTest::addColumn<QString>("channel"); - QTest::addColumn<QString>("channelUrl"); - QTest::addColumn<bool>("hasChannels"); - QTest::addColumn<bool>("valid"); - QTest::addColumn<QList<UpdateChecker::ChannelListEntry> >("result"); - - QTest::newRow("garbage") - << QString() - << findTestDataUrl("data/garbageChannels.json") - << false - << false - << QList<UpdateChecker::ChannelListEntry>(); - QTest::newRow("errors") - << QString() - << findTestDataUrl("data/errorChannels.json") - << false - << true - << QList<UpdateChecker::ChannelListEntry>(); - QTest::newRow("no channels") - << QString() - << findTestDataUrl("data/noChannels.json") - << false - << true - << QList<UpdateChecker::ChannelListEntry>(); - QTest::newRow("one channel") - << QString("develop") - << findTestDataUrl("data/oneChannel.json") - << true - << true - << (QList<UpdateChecker::ChannelListEntry>() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"}); - QTest::newRow("several channels") - << QString("develop") - << findTestDataUrl("data/channels.json") - << true - << true - << (QList<UpdateChecker::ChannelListEntry>() - << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", findTestDataUrl("data")} - << UpdateChecker::ChannelListEntry{"stable", "Stable", "It's stable at least", findTestDataUrl("data")} - << UpdateChecker::ChannelListEntry{"42", "The Channel", "This is the channel that is going to answer all of your questions", "https://dent.me/tea"}); - } - void tst_ChannelListParsing() - { - - QFETCH(QString, channel); - QFETCH(QString, channelUrl); - QFETCH(bool, hasChannels); - QFETCH(bool, valid); - QFETCH(QList<UpdateChecker::ChannelListEntry>, result); - - shared_qobject_ptr<QNetworkAccessManager> nam = new QNetworkAccessManager(); - UpdateChecker checker(nam, channelUrl, channel, 0); - - QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); - QVERIFY(channelListLoadedSpy.isValid()); - - checker.updateChanList(false); - - if (valid) - { - QVERIFY(channelListLoadedSpy.wait()); - QCOMPARE(channelListLoadedSpy.size(), 1); - } - else - { - channelListLoadedSpy.wait(); - QCOMPARE(channelListLoadedSpy.size(), 0); - } - - QCOMPARE(checker.hasChannels(), hasChannels); - QCOMPARE(checker.getChannelList(), result); - } - - void tst_UpdateChecking() - { - QString channel = "develop"; - QString channelUrl = findTestDataUrl("data/channels.json"); - int currentBuild = 2; - - shared_qobject_ptr<QNetworkAccessManager> nam = new QNetworkAccessManager(); - UpdateChecker checker(nam, channelUrl, channel, currentBuild); - - QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(GoUpdate::Status))); - QVERIFY(updateAvailableSpy.isValid()); - QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); - QVERIFY(channelListLoadedSpy.isValid()); - - checker.updateChanList(false); - QVERIFY(channelListLoadedSpy.wait()); - - qDebug() << "CWD:" << QDir::current().absolutePath(); - checker.m_channels[0].url = findTestDataUrl("data/"); - checker.checkForUpdate(channel, false); - - QVERIFY(updateAvailableSpy.wait()); - - auto status = updateAvailableSpy.first().first().value<GoUpdate::Status>(); - QCOMPARE(checker.m_channels[0].url, status.newRepoUrl); - QCOMPARE(3, status.newVersionId); - QCOMPARE(currentBuild, status.currentVersionId); - } -}; - -QTEST_GUILESS_MAIN(UpdateCheckerTest) - -#include "UpdateChecker_test.moc" diff --git a/launcher/updater/testdata/1.json b/launcher/updater/testdata/1.json deleted file mode 100644 index 7af7e52d..00000000 --- a/launcher/updater/testdata/1.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "ApiVersion": 0, - "Id": 1, - "Name": "1.0.1", - "Files": [ - { - "Path": "fileOne", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileOneA" - } - ], - "Executable": true, - "Perms": 493, - "MD5": "9eb84090956c484e32cb6c08455a667b" - }, - { - "Path": "fileTwo", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileTwo" - } - ], - "Executable": false, - "Perms": 644, - "MD5": "38f94f54fa3eb72b0ea836538c10b043" - }, - { - "Path": "fileThree", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileThree" - } - ], - "Executable": false, - "Perms": "750", - "MD5": "f12df554b21e320be6471d7154130e70" - } - ] -} diff --git a/launcher/updater/testdata/2.json b/launcher/updater/testdata/2.json deleted file mode 100644 index 96d430d5..00000000 --- a/launcher/updater/testdata/2.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "ApiVersion": 0, - "Id": 1, - "Name": "1.0.1", - "Files": [ - { - "Path": "fileOne", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileOneB" - } - ], - "Executable": true, - "Perms": 493, - "MD5": "42915a71277c9016668cce7b82c6b577" - }, - { - "Path": "fileTwo", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileTwo" - } - ], - "Executable": false, - "Perms": 644, - "MD5": "38f94f54fa3eb72b0ea836538c10b043" - } - ] -} diff --git a/launcher/updater/testdata/channels.json b/launcher/updater/testdata/channels.json deleted file mode 100644 index 5c6e42cb..00000000 --- a/launcher/updater/testdata/channels.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "develop", - "name": "Develop", - "description": "The channel called \"develop\"", - "url": "@TEST_DATA_URL@" - }, - { - "id": "stable", - "name": "Stable", - "description": "It's stable at least", - "url": "@TEST_DATA_URL@" - }, - { - "id": "42", - "name": "The Channel", - "description": "This is the channel that is going to answer all of your questions", - "url": "https://dent.me/tea" - } - ] -} diff --git a/launcher/updater/testdata/errorChannels.json b/launcher/updater/testdata/errorChannels.json deleted file mode 100644 index a2cb2165..00000000 --- a/launcher/updater/testdata/errorChannels.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "", - "name": "Develop", - "description": "The channel called \"develop\"", - "url": "http://example.org/stuff" - }, - { - "id": "stable", - "name": "", - "description": "It's stable at least", - "url": "ftp://username@host/path/to/stuff" - }, - { - "id": "42", - "name": "The Channel", - "description": "This is the channel that is going to answer all of your questions", - "url": "" - } - ] -} diff --git a/launcher/updater/testdata/fileOneA b/launcher/updater/testdata/fileOneA deleted file mode 100644 index f2e41136..00000000 --- a/launcher/updater/testdata/fileOneA +++ /dev/null @@ -1 +0,0 @@ -stuff diff --git a/launcher/updater/testdata/fileOneB b/launcher/updater/testdata/fileOneB deleted file mode 100644 index f9aba922..00000000 --- a/launcher/updater/testdata/fileOneB +++ /dev/null @@ -1,3 +0,0 @@ -stuff - -more stuff that came in the new version diff --git a/launcher/updater/testdata/fileThree b/launcher/updater/testdata/fileThree deleted file mode 100644 index 6353ff16..00000000 --- a/launcher/updater/testdata/fileThree +++ /dev/null @@ -1 +0,0 @@ -this is yet another file diff --git a/launcher/updater/testdata/fileTwo b/launcher/updater/testdata/fileTwo deleted file mode 100644 index aad9a93a..00000000 --- a/launcher/updater/testdata/fileTwo +++ /dev/null @@ -1 +0,0 @@ -some other stuff diff --git a/launcher/updater/testdata/garbageChannels.json b/launcher/updater/testdata/garbageChannels.json deleted file mode 100644 index 34437451..00000000 --- a/launcher/updater/testdata/garbageChannels.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "develop", - "name": "Develop", - "description": "The channel called \"develop\"", -aa "url": "http://example.org/stuff" - }, -a "id": "stable", - "name": "Stable", - "description": "It's stable at least", - "url": "ftp://username@host/path/to/stuff" - }, - { - "id": "42"f - "name": "The Channel", - "description": "This is the channel that is going to answer all of your questions", - "url": "https://dent.me/tea" - } - ] -} diff --git a/launcher/updater/testdata/index.json b/launcher/updater/testdata/index.json deleted file mode 100644 index 867bdcfb..00000000 --- a/launcher/updater/testdata/index.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ApiVersion": 0, - "Versions": [ - { "Id": 0, "Name": "1.0.0" }, - { "Id": 1, "Name": "1.0.1" }, - { "Id": 2, "Name": "1.0.2" }, - { "Id": 3, "Name": "1.0.3" } - ] -} diff --git a/launcher/updater/testdata/noChannels.json b/launcher/updater/testdata/noChannels.json deleted file mode 100644 index 76988982..00000000 --- a/launcher/updater/testdata/noChannels.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "format_version": 0, - "channels": [ - ] -} diff --git a/launcher/updater/testdata/oneChannel.json b/launcher/updater/testdata/oneChannel.json deleted file mode 100644 index cc8ed255..00000000 --- a/launcher/updater/testdata/oneChannel.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "develop", - "name": "Develop", - "description": "The channel called \"develop\"", - "url": "http://example.org/stuff" - } - ] -} diff --git a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml deleted file mode 100644 index 38ecc809..00000000 --- a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml +++ /dev/null @@ -1,17 +0,0 @@ -<update version="3"> - <install> - <file> - <source>sourceOne</source> - <dest>destOne</dest> - <mode>0777</mode> - </file> - <file> - <source>PolyMC.exe</source> - <dest>P/o/l/y/M/C/e/x/e</dest> - <mode>0644</mode> - </file> - </install> - <uninstall> - <file>toDelete.abc</file> - </uninstall> -</update> |