diff options
Diffstat (limited to 'launcher')
200 files changed, 6901 insertions, 1681 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c6e04a85..cb8088be 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -84,6 +84,7 @@ #include <QDebug> #include <QStyleFactory> #include <QWindow> +#include <QIcon> #include "InstanceList.h" @@ -99,7 +100,6 @@ #include "tools/JVisualVM.h" #include "tools/MCEditTool.h" -#include <xdgicon.h> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -321,7 +321,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { // Root path is used for updates and portable data -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) @@ -543,6 +543,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this)); // Updates + // Multiple channels are separated by spaces m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL); m_settings->registerSetting("AutoUpdate", true); @@ -710,9 +711,20 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); - // Custom MSA credentials + // Custom Microsoft Authentication Client ID m_settings->registerSetting("MSAClientIDOverride", ""); - m_settings->registerSetting("CFKeyOverride", ""); + + // Custom Flame API Key + { + m_settings->registerSetting("CFKeyOverride", ""); + m_settings->registerSetting("FlameKeyOverride", ""); + + QString flameKey = m_settings->get("CFKeyOverride").toString(); + + if (!flameKey.isEmpty()) + m_settings->set("FlameKeyOverride", flameKey); + m_settings->reset("CFKeyOverride"); + } m_settings->registerSetting("UserAgentOverride", ""); // Init page provider @@ -852,6 +864,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); + m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath()); m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); @@ -1182,7 +1195,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) @@ -1190,7 +1203,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) @@ -1246,6 +1259,9 @@ bool Application::launch( } connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); + connect(controller.get(), &LaunchController::aborted, this, [this] { + controllerFailed(tr("Aborted")); + }); addRunningInstance(); controller->start(); return true; @@ -1552,6 +1568,16 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex() return m_metadataIndex; } +Application::Capabilities Application::currentCapabilities() +{ + Capabilities c; + if (!getMSAClientID().isEmpty()) + c |= SupportsMSA; + if (!getFlameAPIKey().isEmpty()) + c |= SupportsFlame; + return c; +} + QString Application::getJarPath(QString jarFile) { QStringList potentialPaths = { @@ -1580,14 +1606,14 @@ QString Application::getMSAClientID() return BuildConfig.MSA_CLIENT_ID; } -QString Application::getCurseKey() +QString Application::getFlameAPIKey() { - QString keyOverride = m_settings->get("CFKeyOverride").toString(); + QString keyOverride = m_settings->get("FlameKeyOverride").toString(); if (!keyOverride.isEmpty()) { return keyOverride; } - return BuildConfig.CURSEFORGE_API_KEY; + return BuildConfig.FLAME_API_KEY; } QString Application::getUserAgent() diff --git a/launcher/Application.h b/launcher/Application.h index e3b4fced..019c3c3d 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -90,6 +90,14 @@ public: Initialized }; + enum Capability { + None = 0, + + SupportsMSA = 1 << 0, + SupportsFlame = 1 << 1, + }; + Q_DECLARE_FLAGS(Capabilities, Capability) + public: Application(int &argc, char **argv); virtual ~Application(); @@ -154,6 +162,8 @@ public: shared_qobject_ptr<Meta::Index> metadataIndex(); + Capabilities currentCapabilities(); + /*! * Finds and returns the full path to a jar file. * Returns a null-string if it could not be found. @@ -161,7 +171,7 @@ public: QString getJarPath(QString jarFile); QString getMSAClientID(); - QString getCurseKey(); + QString getFlameAPIKey(); QString getUserAgent(); QString getUserAgentUncached(); 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 0efbdddc..5a84a931 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -39,6 +39,7 @@ #include <QFileInfo> #include <QDir> #include <QDebug> +#include <QRegularExpression> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -335,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/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 eeb786a6..4ce033f9 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -88,10 +88,10 @@ set(CORE_SOURCES MMCTime.cpp ) -ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME FileSystem) # TODO: needs testdata -ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GZip) set(PATHMATCHER_SOURCES @@ -117,6 +117,7 @@ set(NET_SOURCES net/NetAction.h net/NetJob.cpp net/NetJob.h + net/NetUtils.h net/PasteUpload.cpp net/PasteUpload.h net/Sink.h @@ -157,6 +158,12 @@ set(UPDATE_SOURCES updater/UpdateChecker.cpp updater/DownloadTask.h updater/DownloadTask.cpp + updater/ExternalUpdater.h +) + +set(MAC_UPDATE_SOURCES + updater/MacSparkleUpdater.h + updater/MacSparkleUpdater.mm ) # Backend for the news bar... there's usually no news. @@ -338,7 +345,7 @@ set(MINECRAFT_SOURCES mojang/PackageManifest.cpp minecraft/Agent.h) -ecm_add_test(minecraft/GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(minecraft/GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GradleSpecifier) if(BUILD_TESTING) @@ -347,7 +354,7 @@ if(BUILD_TESTING) ) target_link_libraries(PackageManifest Launcher_logic - Qt5::Test + Qt${QT_VERSION_MAJOR}::Test ) target_include_directories(PackageManifest PRIVATE ../cmake/UnitTest/ @@ -360,18 +367,18 @@ if(BUILD_TESTING) endif() # TODO: needs minecraft/testdata -ecm_add_test(minecraft/MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(minecraft/MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MojangVersionFormat) -ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Library) # FIXME: shares data with FileSystem test # TODO: needs testdata -ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ModFolderModel) -ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ParseUtils) # the screenshots feature @@ -391,9 +398,11 @@ set(TASKS_SOURCES tasks/ConcurrentTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp + tasks/MultipleOptionsTask.h + tasks/MultipleOptionsTask.cpp ) -ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Task) set(SETTINGS_SOURCES @@ -412,7 +421,7 @@ set(SETTINGS_SOURCES settings/SettingsObject.h ) -ecm_add_test(settings/INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(settings/INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME INIFile) set(JAVA_SOURCES @@ -430,7 +439,7 @@ set(JAVA_SOURCES java/JavaVersion.cpp ) -ecm_add_test(java/JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(java/JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME JavaVersion) set(TRANSLATIONS_SOURCES @@ -474,9 +483,15 @@ set(API_SOURCES modplatform/ModAPI.h + modplatform/EnsureMetadataTask.h + modplatform/EnsureMetadataTask.cpp + + modplatform/CheckUpdateTask.h + modplatform/flame/FlameAPI.h + modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h - + modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkModAPI.cpp ) @@ -502,6 +517,8 @@ set(FLAME_SOURCES modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp + modplatform/flame/FlameCheckUpdate.cpp + modplatform/flame/FlameCheckUpdate.h ) set(MODRINTH_SOURCES @@ -509,6 +526,8 @@ set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.h modplatform/modrinth/ModrinthPackManifest.cpp modplatform/modrinth/ModrinthPackManifest.h + modplatform/modrinth/ModrinthCheckUpdate.cpp + modplatform/modrinth/ModrinthCheckUpdate.h ) set(MODPACKSCH_SOURCES @@ -524,7 +543,7 @@ set(PACKWIZ_SOURCES ) # TODO: needs modplatform/packwiz/testdata -ecm_add_test(modplatform/packwiz/Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(modplatform/packwiz/Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Packwiz) set(TECHNIC_SOURCES @@ -549,7 +568,7 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) -ecm_add_test(meta/Index_test.cpp LINK_LIBRARIES Launcher_logic Qt5::Test +ecm_add_test(meta/Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Index) ################################ COMPILE ################################ @@ -583,6 +602,10 @@ set(LOGIC_SOURCES ${ATLAUNCHER_SOURCES} ) +if(APPLE) + set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) +endif() + SET(LAUNCHER_SOURCES # Application base Application.h @@ -828,6 +851,10 @@ SET(LAUNCHER_SOURCES ui/dialogs/ModDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.h + ui/dialogs/ChooseProviderDialog.h + ui/dialogs/ChooseProviderDialog.cpp + ui/dialogs/ModUpdateDialog.cpp + ui/dialogs/ModUpdateDialog.h # GUI - widgets ui/widgets/Common.cpp @@ -882,7 +909,7 @@ 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 @@ -933,9 +960,10 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui + ui/dialogs/ChooseProviderDialog.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 @@ -959,12 +987,13 @@ add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHE target_link_libraries(Launcher_logic systeminfo Launcher_classparser + Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} - optional-bare tomlc99 BuildConfig Katabasis + Qt${QT_VERSION_MAJOR}::Widgets ) if (UNIX AND NOT CYGWIN AND NOT APPLE) @@ -974,19 +1003,36 @@ if (UNIX AND NOT CYGWIN AND NOT APPLE) 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 Launcher_rainbow ) +if(APPLE) + set(CMAKE_MACOSX_RPATH 1) + set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") + + file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) + file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) + + find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + target_link_libraries(Launcher_logic + "-framework AppKit" + "-framework Carbon" + "-framework Foundation" + "-framework ApplicationServices" + ) + target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) +endif() target_link_libraries(Launcher_logic) @@ -1009,8 +1055,16 @@ install(TARGETS ${Launcher_Name} BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) +if (UNIX AND APPLE) + # Add Sparkle updater + # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of + # the framework aren't installed + install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) +endif() + #### The bundle mess! #### # Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. @@ -1051,6 +1105,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( @@ -1093,6 +1155,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 3837d75f..21edbb48 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -1,77 +1,98 @@ -// 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" +#include <QDebug> #include <QDir> #include <QFile> -#include <QSaveFile> #include <QFileInfo> -#include <QDebug> -#include <QUrl> +#include <QSaveFile> #include <QStandardPaths> #include <QTextStream> +#include <QUrl> #if defined Q_OS_WIN32 - #include <windows.h> - #include <string> - #include <sys/utime.h> - #include <winnls.h> - #include <shobjidl.h> - #include <objbase.h> - #include <objidl.h> - #include <shlguid.h> - #include <shlobj.h> +#include <objbase.h> +#include <objidl.h> +#include <shlguid.h> +#include <shlobj.h> +#include <shobjidl.h> +#include <sys/utime.h> +#include <windows.h> +#include <winnls.h> +#include <string> #else - #include <utime.h> +#include <utime.h> #endif namespace FS { -void ensureExists(const QDir &dir) +void ensureExists(const QDir& dir) { - if (!QDir().mkpath(dir.absolutePath())) - { - throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + - dir.absolutePath() + ")"); + if (!QDir().mkpath(dir.absolutePath())) { + throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")"); } } -void write(const QString &filename, const QByteArray &data) +void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) - { - throw FileSystemException("Couldn't open " + filename + " for writing: " + - file.errorString()); + if (!file.open(QSaveFile::WriteOnly)) { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } - if (data.size() != file.write(data)) - { - throw FileSystemException("Error writing data to " + filename + ": " + - file.errorString()); + if (data.size() != file.write(data)) { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } - if (!file.commit()) - { - throw FileSystemException("Error while committing data to " + filename + ": " + - file.errorString()); + if (!file.commit()) { + throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } -QByteArray read(const QString &filename) +QByteArray read(const QString& filename) { QFile file(filename); - if (!file.open(QFile::ReadOnly)) - { - throw FileSystemException("Unable to open " + filename + " for reading: " + - file.errorString()); + if (!file.open(QFile::ReadOnly)) { + throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString()); } const qint64 size = file.size(); QByteArray data(int(size), 0); const qint64 ret = file.read(data.data(), size); - if (ret == -1 || ret != size) - { - throw FileSystemException("Error reading data from " + filename + ": " + - file.errorString()); + if (ret == -1 || ret != size) { + throw FileSystemException("Error reading data from " + filename + ": " + file.errorString()); } return data; } @@ -105,12 +126,12 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -bool copy::operator()(const QString &offset) +bool copy::operator()(const QString& offset) { - //NOTE always deep copy on windows. the alternatives are too messy. - #if defined Q_OS_WIN32 +// NOTE always deep copy on windows. the alternatives are too messy. +#if defined Q_OS_WIN32 m_followSymlinks = true; - #endif +#endif auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); @@ -119,52 +140,39 @@ bool copy::operator()(const QString &offset) if (!currentSrc.exists()) return false; - if(!m_followSymlinks && currentSrc.isSymLink()) - { + if (!m_followSymlinks && currentSrc.isSymLink()) { qDebug() << "creating symlink" << src << " - " << dst; - if (!ensureFilePathExists(dst)) - { + if (!ensureFilePathExists(dst)) { qWarning() << "Cannot create path!"; return false; } return QFile::link(currentSrc.symLinkTarget(), dst); - } - else if(currentSrc.isFile()) - { + } else if (currentSrc.isFile()) { qDebug() << "copying file" << src << " - " << dst; - if (!ensureFilePathExists(dst)) - { + if (!ensureFilePathExists(dst)) { qWarning() << "Cannot create path!"; return false; } return QFile::copy(src, dst); - } - else if(currentSrc.isDir()) - { + } else if (currentSrc.isDir()) { qDebug() << "recursing" << offset; - if (!ensureFolderPathExists(dst)) - { + if (!ensureFolderPathExists(dst)) { qWarning() << "Cannot create path!"; return false; } QDir currentDir(src); - for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) - { + for (auto& f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) { auto inner_offset = PathCombine(offset, f); // ignore and skip stuff that matches the blacklist. - if(m_blacklist && m_blacklist->matches(inner_offset)) - { + if (m_blacklist && m_blacklist->matches(inner_offset)) { continue; } - if(!operator()(inner_offset)) - { + if (!operator()(inner_offset)) { qWarning() << "Failed to copy" << inner_offset; return false; } } - } - else - { + } else { qCritical() << "Copy ERROR: Unknown filesystem object:" << src; return false; } @@ -175,55 +183,41 @@ bool deletePath(QString path) { bool OK = true; QFileInfo finfo(path); - if(finfo.isFile()) { + if (finfo.isFile()) { return QFile::remove(path); } QDir dir(path); - if (!dir.exists()) - { + if (!dir.exists()) { return OK; } - auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | - QDir::AllDirs | QDir::Files, - QDir::DirsFirst); + auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst); - for(auto & info: allEntries) - { + for (auto& info : allEntries) { #if defined Q_OS_WIN32 QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath()); auto wString = nativePath.toStdWString(); DWORD dwAttrs = GetFileAttributesW(wString.c_str()); // Windows: check for junctions, reparse points and other nasty things of that sort - if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) - { - if (info.isFile()) - { + if (dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) { + if (info.isFile()) { OK &= QFile::remove(info.absoluteFilePath()); - } - else if (info.isDir()) - { + } else if (info.isDir()) { OK &= dir.rmdir(info.absoluteFilePath()); } } #else // We do not trust Qt with reparse points, but do trust it with unix symlinks. - if(info.isSymLink()) - { + if (info.isSymLink()) { OK &= QFile::remove(info.absoluteFilePath()); } #endif - else if (info.isDir()) - { + else if (info.isDir()) { OK &= deletePath(info.absoluteFilePath()); - } - else if (info.isFile()) - { + } else if (info.isFile()) { OK &= QFile::remove(info.absoluteFilePath()); - } - else - { + } else { OK = false; qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath(); } @@ -232,22 +226,30 @@ bool deletePath(QString path) return OK; } +bool trash(QString path, QString *pathInTrash = nullptr) +{ +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + return false; +#else + return QFile::moveToTrash(path, pathInTrash); +#endif +} -QString PathCombine(const QString & path1, const QString & path2) +QString PathCombine(const QString& path1, const QString& path2) { - if(!path1.size()) + if (!path1.size()) return path2; - if(!path2.size()) + if (!path2.size()) return path1; return QDir::cleanPath(path1 + QDir::separator() + path2); } -QString PathCombine(const QString & path1, const QString & path2, const QString & path3) +QString PathCombine(const QString& path1, const QString& path2, const QString& path3) { return PathCombine(PathCombine(path1, path2), path3); } -QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4) +QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4) { return PathCombine(PathCombine(path1, path2, path3), path4); } @@ -259,17 +261,14 @@ QString AbsolutePath(QString path) QString ResolveExecutable(QString path) { - if (path.isEmpty()) - { + if (path.isEmpty()) { return QString(); } - if(!path.contains('/')) - { + if (!path.contains('/')) { path = QStandardPaths::findExecutable(path); } QFileInfo pathInfo(path); - if(!pathInfo.exists() || !pathInfo.isExecutable()) - { + if (!pathInfo.exists() || !pathInfo.isExecutable()) { return QString(); } return pathInfo.absoluteFilePath(); @@ -289,12 +288,9 @@ QString NormalizePath(QString path) QDir b(path); QString newAbsolute = b.absolutePath(); - if (newAbsolute.startsWith(currentAbsolute)) - { + if (newAbsolute.startsWith(currentAbsolute)) { return a.relativeFilePath(newAbsolute); - } - else - { + } else { return newAbsolute; } } @@ -303,10 +299,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { - for (int i = 0; i < string.length(); i++) - { - if (badFilenameChars.contains(string[i])) - { + for (int i = 0; i < string.length(); i++) { + if (badFilenameChars.contains(string[i])) { string[i] = replaceWith; } } @@ -318,15 +312,12 @@ QString DirNameFromString(QString string, QString inDir) int num = 0; QString baseName = RemoveInvalidFilenameChars(string, '-'); QString dirName; - do - { - if(num == 0) - { + do { + if (num == 0) { dirName = baseName; - } - else - { - dirName = baseName + QString::number(num);; + } else { + dirName = baseName + QString::number(num); + ; } // If it's over 9000 @@ -346,40 +337,35 @@ bool checkProblemticPathJava(QDir folder) } // Win32 crap -#if defined Q_OS_WIN +#ifdef Q_OS_WIN bool called_coinit = false; -HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args) +HRESULT CreateLink(LPCCH linkPath, LPCWSTR targetPath, LPCWSTR args) { HRESULT hres; - if (!called_coinit) - { + if (!called_coinit) { hres = CoInitialize(NULL); called_coinit = true; - if (!SUCCEEDED(hres)) - { + if (!SUCCEEDED(hres)) { qWarning("Failed to initialize COM. Error 0x%08lX", hres); return hres; } } - IShellLink *link; - hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, - (LPVOID *)&link); + IShellLink* link; + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&link); - if (SUCCEEDED(hres)) - { - IPersistFile *persistFile; + if (SUCCEEDED(hres)) { + IPersistFile* persistFile; link->SetPath(targetPath); link->SetArguments(args); - hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile); - if (SUCCEEDED(hres)) - { + hres = link->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); + if (SUCCEEDED(hres)) { WCHAR wstr[MAX_PATH]; MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH); @@ -400,8 +386,7 @@ QString getDesktopDir() } // Cross-platform Shortcut creation -bool createShortCut(QString location, QString dest, QStringList args, QString name, - QString icon) +bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) location = PathCombine(location, name + ".desktop"); @@ -426,8 +411,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na stream.flush(); f.close(); - f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | - QFileDevice::ExeOther); + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return true; #elif defined Q_OS_WIN diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 31c7af70..b46f3281 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 @@ -8,29 +41,27 @@ #include <QDir> #include <QFlags> -namespace FS -{ +namespace FS { -class FileSystemException : public ::Exception -{ -public: - FileSystemException(const QString &message) : Exception(message) {} +class FileSystemException : public ::Exception { + public: + FileSystemException(const QString& message) : Exception(message) {} }; /** * write data to a file safely */ -void write(const QString &filename, const QByteArray &data); +void write(const QString& filename, const QByteArray& data); /** * read data from a file safely\ */ -QByteArray read(const QString &filename); +QByteArray read(const QString& filename); /** * Update the last changed timestamp of an existing file */ -bool updateTimestamp(const QString & filename); +bool updateTimestamp(const QString& filename); /** * Creates all the folders in a path for the specified path @@ -44,35 +75,31 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); -class copy -{ -public: - copy(const QString & src, const QString & dst) +class copy { + public: + copy(const QString& src, const QString& dst) { - m_src = src; - m_dst = dst; + m_src.setPath(src); + m_dst.setPath(dst); } - copy & followSymlinks(const bool follow) + copy& followSymlinks(const bool follow) { m_followSymlinks = follow; return *this; } - copy & blacklist(const IPathMatcher * filter) + copy& blacklist(const IPathMatcher* filter) { m_blacklist = filter; return *this; } - bool operator()() - { - return operator()(QString()); - } + bool operator()() { return operator()(QString()); } -private: - bool operator()(const QString &offset); + private: + bool operator()(const QString& offset); -private: + private: bool m_followSymlinks = true; - const IPathMatcher * m_blacklist = nullptr; + const IPathMatcher* m_blacklist = nullptr; QDir m_src; QDir m_dst; }; @@ -82,9 +109,14 @@ private: */ bool deletePath(QString path); -QString PathCombine(const QString &path1, const QString &path2); -QString PathCombine(const QString &path1, const QString &path2, const QString &path3); -QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4); +/** + * Trash a folder / file + */ +bool trash(QString path, QString *pathInTrash); + +QString PathCombine(const QString& path1, const QString& path2); +QString PathCombine(const QString& path1, const QString& path2, const QString& path3); +QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4); QString AbsolutePath(QString path); diff --git a/launcher/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/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 6b0f08d1..14e1cd47 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -325,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(); @@ -722,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); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index b67d48f3..48ba2161 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -44,7 +44,7 @@ #include "QObjectPtr.h" #include "modplatform/flame/PackManifest.h" -#include <nonstd/optional> +#include <optional> class QuaZip; namespace Flame @@ -90,8 +90,8 @@ private: /* data */ QString m_archivePath; bool m_downloadRequired = false; std::unique_ptr<QuaZip> m_packZip; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; QVector<Flame::File> m_blockedMods; enum class ModpackType{ Unknown, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 3e3c81f7..4447a17c 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -1,42 +1,64 @@ -/* 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 <QDebug> #include <QDir> #include <QDirIterator> -#include <QSet> #include <QFile> -#include <QThread> -#include <QTextStream> -#include <QXmlStreamReader> -#include <QTimer> -#include <QDebug> #include <QFileSystemWatcher> -#include <QUuid> #include <QJsonArray> #include <QJsonDocument> #include <QMimeData> +#include <QSet> +#include <QStack> +#include <QPair> +#include <QTextStream> +#include <QThread> +#include <QTimer> +#include <QUuid> +#include <QXmlStreamReader> -#include "InstanceList.h" #include "BaseInstance.h" +#include "ExponentialSeries.h" +#include "FileSystem.h" +#include "InstanceList.h" #include "InstanceTask.h" -#include "settings/INISettingsObject.h" #include "NullInstance.h" -#include "minecraft/MinecraftInstance.h" -#include "FileSystem.h" -#include "ExponentialSeries.h" #include "WatchLock.h" +#include "minecraft/MinecraftInstance.h" +#include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 #include <Windows.h> @@ -44,13 +66,12 @@ const static int GROUP_FILE_FORMAT_VERSION = 1; -InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) +InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); // Create aand normalize path - if (!QDir::current().exists(instDir)) - { + if (!QDir::current().exists(instDir)) { QDir::current().mkpath(instDir); } @@ -63,9 +84,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, m_watcher->addPath(m_instDir); } -InstanceList::~InstanceList() -{ -} +InstanceList::~InstanceList() {} Qt::DropActions InstanceList::supportedDragActions() const { @@ -79,7 +98,7 @@ Qt::DropActions InstanceList::supportedDropActions() const bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const { - if(data && data->hasFormat("application/x-instanceid")) { + if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; @@ -87,7 +106,7 @@ bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { - if(data && data->hasFormat("application/x-instanceid")) { + if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; @@ -100,35 +119,33 @@ QStringList InstanceList::mimeTypes() const return types; } -QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const +QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const { auto mimeData = QAbstractListModel::mimeData(indexes); - if(indexes.size() == 1) { + if (indexes.size() == 1) { auto instanceId = data(indexes[0], InstanceIDRole).toString(); mimeData->setData("application/x-instanceid", instanceId.toUtf8()); } return mimeData; } - -int InstanceList::rowCount(const QModelIndex &parent) const +int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_instances.count(); } -QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const +QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); if (row < 0 || row >= m_instances.size()) return QModelIndex(); - return createIndex(row, column, (void *)m_instances.at(row).get()); + return createIndex(row, column, (void*)m_instances.at(row).get()); } -QVariant InstanceList::data(const QModelIndex &index, int role) const +QVariant InstanceList::data(const QModelIndex& index, int role) const { - if (!index.isValid()) - { + if (!index.isValid()) { return QVariant(); } BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer()); @@ -136,7 +153,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: @@ -173,29 +190,25 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid()) - { + if (!index.isValid()) { return false; } - if(role != Qt::EditRole) - { + if (role != Qt::EditRole) { return false; } - BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer()); + BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer()); auto newName = value.toString(); - if(pdata->name() == newName) - { + if (pdata->name() == newName) { return true; } pdata->setName(newName); return true; } -Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const +Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const { Qt::ItemFlags f; - if (index.isValid()) - { + if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; @@ -204,13 +217,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const GroupId InstanceList::getInstanceGroup(const InstanceId& id) const { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { return GroupId(); } auto iter = m_instanceGroupIndex.find(inst->id()); - if(iter != m_instanceGroupIndex.end()) - { + if (iter != m_instanceGroupIndex.end()) { return *iter; } return GroupId(); @@ -219,64 +230,54 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { qDebug() << "Attempt to set a null instance's group"; return; } bool changed = false; auto iter = m_instanceGroupIndex.find(inst->id()); - if(iter != m_instanceGroupIndex.end()) - { - if(*iter != name) - { + if (iter != m_instanceGroupIndex.end()) { + if (*iter != name) { *iter = name; changed = true; } - } - else - { + } else { changed = true; m_instanceGroupIndex[id] = name; } - if(changed) - { + if (changed) { m_groupNameCache.insert(name); auto idx = getInstIndex(inst.get()); - emit dataChanged(index(idx), index(idx), {GroupRole}); + emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } } QStringList InstanceList::getGroups() { - return m_groupNameCache.toList(); + return m_groupNameCache.values(); } void InstanceList::deleteGroup(const QString& name) { bool removed = false; qDebug() << "Delete group" << name; - for(auto & instance: m_instances) - { - const auto & instID = instance->id(); + for (auto& instance : m_instances) { + const auto& instID = instance->id(); auto instGroupName = getInstanceGroup(instID); - if(instGroupName == name) - { + if (instGroupName == name) { m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); - if(idx > 0) - { - emit dataChanged(index(idx), index(idx), {GroupRole}); + if (idx > 0) { + emit dataChanged(index(idx), index(idx), { GroupRole }); } } } - if(removed) - { + if (removed) { saveGroupList(); } } @@ -286,23 +287,75 @@ bool InstanceList::isGroupCollapsed(const QString& group) return m_collapsedGroups.contains(group); } +bool InstanceList::trashInstance(const InstanceId& id) +{ + auto inst = getInstanceById(id); + if (!inst) { + qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + return false; + } + + auto cachedGroupId = m_instanceGroupIndex[id]; + + qDebug() << "Will trash instance" << id; + QString trashedLoc; + + if (m_instanceGroupIndex.remove(id)) { + saveGroupList(); + } + + if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { + qDebug() << "Trash of instance" << id << "has not been completely successfully..."; + return false; + } + + qDebug() << "Instance" << id << "has been trashed by the launcher."; + m_trashHistory.push({id, inst->instanceRoot(), trashedLoc, cachedGroupId}); + + return true; +} + +bool InstanceList::trashedSomething() { + return !m_trashHistory.empty(); +} + +void InstanceList::undoTrashInstance() { + if (m_trashHistory.empty()) { + qWarning() << "Nothing to recover from trash."; + return; + } + + auto top = m_trashHistory.pop(); + + while (QDir(top.polyPath).exists()) { + top.id += "1"; + top.polyPath += "1"; + } + + qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath; + QFile(top.trashPath).rename(top.polyPath); + + m_instanceGroupIndex[top.id] = top.groupName; + m_groupNameCache.insert(top.groupName); + + saveGroupList(); + emit instancesChanged(); +} + void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } - if(m_instanceGroupIndex.remove(id)) - { + if (m_instanceGroupIndex.remove(id)) { saveGroupList(); } qDebug() << "Will delete instance" << id; - if(!FS::deletePath(inst->instanceRoot())) - { + if (!FS::deletePath(inst->instanceRoot())) { qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; return; } @@ -310,15 +363,13 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Instance" << id << "has been deleted by the launcher."; } -static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list) +static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr>& list) { QMap<InstanceId, InstanceLocator> out; int i = 0; - for(auto & item: list) - { + for (auto& item : list) { auto id = item->id(); - if(out.contains(id)) - { + if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } out[id] = std::make_pair(item, i); @@ -327,24 +378,21 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> & return out; } -QList< InstanceId > InstanceList::discoverInstances() +QList<InstanceId> InstanceList::discoverInstances() { qDebug() << "Discovering instances in" << m_instDir; QList<InstanceId> out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); - while (iter.hasNext()) - { + while (iter.hasNext()) { QString subDir = iter.next(); QFileInfo dirInfo(subDir); if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) continue; // if it is a symlink, ignore it if it goes to the instance folder - if(dirInfo.isSymLink()) - { + if (dirInfo.isSymLink()) { QFileInfo targetInfo(dirInfo.symLinkTarget()); QFileInfo instDirInfo(m_instDir); - if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) - { + if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) { qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; continue; } @@ -353,7 +401,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; } @@ -364,74 +416,56 @@ InstanceList::InstListError InstanceList::loadList() QList<InstancePtr> newList; - for(auto & id: discoverInstances()) - { - if(existingIds.contains(id)) - { + for (auto& id : discoverInstances()) { + if (existingIds.contains(id)) { auto instPair = existingIds[id]; existingIds.remove(id); qDebug() << "Should keep and soft-reload" << id; - } - else - { + } else { InstancePtr instPtr = loadInstance(id); - if(instPtr) - { + if (instPtr) { newList.append(instPtr); } } } // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. - if(!existingIds.isEmpty()) - { + if (!existingIds.isEmpty()) { // get the list of removed instances and sort it by their original index, from last to first auto deadList = existingIds.values(); - auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool - { - return a.second > b.second; - }; + auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; }; std::sort(deadList.begin(), deadList.end(), orderSortPredicate); // remove the contiguous ranges of rows int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; - auto removeNow = [&]() - { + auto removeNow = [&]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); front_bookmark = -1; back_bookmark = currentItem; }; - for(auto & removedItem: deadList) - { + for (auto& removedItem : deadList) { auto instPtr = removedItem.first; instPtr->invalidate(); currentItem = removedItem.second; - if(back_bookmark == -1) - { + if (back_bookmark == -1) { // no bookmark yet back_bookmark = currentItem; - } - else if(currentItem == front_bookmark - 1) - { + } else if (currentItem == front_bookmark - 1) { // part of contiguous sequence, continue - } - else - { + } else { // seam between previous and current item removeNow(); } front_bookmark = currentItem; } - if(back_bookmark != -1) - { + if (back_bookmark != -1) { removeNow(); } } - if(newList.size()) - { + if (newList.size()) { add(newList); } m_dirty = false; @@ -442,26 +476,23 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for(auto const& itr : m_instances) - { + for (auto const& itr : m_instances) { totalPlayTime += itr.get()->totalTimePlayed(); } } void InstanceList::saveNow() { - for(auto & item: m_instances) - { + for (auto& item : m_instances) { item->saveNow(); } } -void InstanceList::add(const QList<InstancePtr> &t) +void InstanceList::add(const QList<InstancePtr>& t) { beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); m_instances.append(t); - for(auto & ptr : t) - { + for (auto& ptr : t) { connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); @@ -469,69 +500,61 @@ void InstanceList::add(const QList<InstancePtr> &t) void InstanceList::resumeWatch() { - if(m_watchLevel > 0) - { + if (m_watchLevel > 0) { qWarning() << "Bad suspend level resume in instance list"; return; } m_watchLevel++; - if(m_watchLevel > 0 && m_dirty) - { + if (m_watchLevel > 0 && m_dirty) { loadList(); } } void InstanceList::suspendWatch() { - m_watchLevel --; + m_watchLevel--; } void InstanceList::providerUpdated() { m_dirty = true; - if(m_watchLevel == 1) - { + if (m_watchLevel == 1) { loadList(); } } InstancePtr InstanceList::getInstanceById(QString instId) const { - if(instId.isEmpty()) + if (instId.isEmpty()) return InstancePtr(); - for(auto & inst: m_instances) - { - if (inst->id() == instId) - { + for (auto& inst : m_instances) { + if (inst->id() == instId) { return inst; } } return InstancePtr(); } -QModelIndex InstanceList::getInstanceIndexById(const QString &id) const +QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { return index(getInstIndex(getInstanceById(id).get())); } -int InstanceList::getInstIndex(BaseInstance *inst) const +int InstanceList::getInstIndex(BaseInstance* inst) const { int count = m_instances.count(); - for (int i = 0; i < count; i++) - { - if (inst == m_instances[i].get()) - { + for (int i = 0; i < count; i++) { + if (inst == m_instances[i].get()) { return i; } } return -1; } -void InstanceList::propertiesChanged(BaseInstance *inst) +void InstanceList::propertiesChanged(BaseInstance* inst) { int i = getInstIndex(inst); - if (i != -1) - { + if (i != -1) { emit dataChanged(index(i), index(i)); updateTotalPlayTime(); } @@ -539,8 +562,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst) InstancePtr InstanceList::loadInstance(const InstanceId& id) { - if(!m_groupsLoaded) - { + if (!m_groupsLoaded) { loadGroupList(); } @@ -568,50 +590,42 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) void InstanceList::saveGroupList() { qDebug() << "Will save group list now."; - if(!m_instancesProbed) - { + if (!m_instancesProbed) { qDebug() << "Group saving prevented because we don't know the full list of instances yet."; return; } WatchLock foo(m_watcher, m_instDir); QString groupFileName = m_instDir + "/instgroups.json"; QMap<QString, QSet<QString>> reverseGroupMap; - for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) - { + for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { QString id = iter.key(); QString group = iter.value(); if (group.isEmpty()) continue; - if(!instanceSet.contains(id)) - { + if (!instanceSet.contains(id)) { qDebug() << "Skipping saving missing instance" << id << "to groups list."; continue; } - if (!reverseGroupMap.count(group)) - { + if (!reverseGroupMap.count(group)) { QSet<QString> set; set.insert(id); reverseGroupMap[group] = set; - } - else - { - QSet<QString> &set = reverseGroupMap[group]; + } else { + QSet<QString>& set = reverseGroupMap[group]; set.insert(id); } } QJsonObject toplevel; toplevel.insert("formatVersion", QJsonValue(QString("1"))); QJsonObject groupsArr; - for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) - { + for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) { auto list = iter.value(); auto name = iter.key(); QJsonObject groupObj; QJsonArray instanceArr; groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); - for (auto item : list) - { + for (auto item : list) { instanceArr.append(QJsonValue(item)); } groupObj.insert("instances", instanceArr); @@ -619,13 +633,10 @@ void InstanceList::saveGroupList() } toplevel.insert("groups", groupsArr); QJsonDocument doc(toplevel); - try - { + try { FS::write(groupFileName, doc.toJson()); qDebug() << "Group list saved."; - } - catch (const FS::FileSystemException &e) - { + } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write instance group file :" << e.cause(); } } @@ -641,12 +652,9 @@ void InstanceList::loadGroupList() return; QByteArray jsonData; - try - { + try { jsonData = FS::read(groupFileName); - } - catch (const FS::FileSystemException &e) - { + } catch (const FS::FileSystemException& e) { qCritical() << "Failed to read instance group file :" << e.cause(); return; } @@ -655,17 +663,15 @@ void InstanceList::loadGroupList() QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); // if the json was bad, fail - if (error.error != QJsonParseError::NoError) - { + if (error.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse instance group file: %1 at offset %2") - .arg(error.errorString(), QString::number(error.offset)) - .toUtf8(); + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); return; } // if the root of the json wasn't an object, fail - if (!jsonDoc.isObject()) - { + if (!jsonDoc.isObject()) { qWarning() << "Invalid group file. Root entry should be an object."; return; } @@ -677,8 +683,7 @@ void InstanceList::loadGroupList() return; // Get the groups. if it's not an object, fail - if (!rootObj.value("groups").isObject()) - { + if (!rootObj.value("groups").isObject()) { qWarning() << "Invalid group list JSON: 'groups' should be an object."; return; } @@ -688,21 +693,20 @@ void InstanceList::loadGroupList() // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); - for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) - { + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { QString groupName = iter.key(); // If not an object, complain and skip to the next one. - if (!iter.value().isObject()) - { + if (!iter.value().isObject()) { qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8(); continue; } QJsonObject groupObj = iter.value().toObject(); - if (!groupObj.value("instances").isArray()) - { - qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8(); + if (!groupObj.value("instances").isArray()) { + qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.") + .arg(groupName) + .toUtf8(); continue; } @@ -710,15 +714,14 @@ void InstanceList::loadGroupList() groupSet.insert(groupName); auto hidden = groupObj.value("hidden").toBool(false); - if(hidden) { + if (hidden) { m_collapsedGroups.insert(groupName); } // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); - for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) - { + for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) { m_instanceGroupIndex[(*iter2).toString()] = groupName; } } @@ -733,13 +736,11 @@ void InstanceList::instanceDirContentsChanged(const QString& path) emit instancesChanged(); } -void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) +void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); - if(newInstDir != m_instDir) - { - if(m_groupsLoaded) - { + if (newInstDir != m_instDir) { + if (m_groupsLoaded) { saveGroupList(); } m_instDir = newInstDir; @@ -751,7 +752,7 @@ void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) { qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); - if(collapsed) { + if (collapsed) { m_collapsedGroups.insert(group); } else { m_collapsedGroups.remove(group); @@ -759,19 +760,14 @@ void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) saveGroupList(); } -class InstanceStaging : public Task -{ -Q_OBJECT +class InstanceStaging : public Task { + Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; -public: - InstanceStaging ( - InstanceList * parent, - Task * child, - const QString & stagingPath, - const QString& instanceName, - const QString& groupName ) - : backoff(minBackoff, maxBackoff) + + public: + InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) + : backoff(minBackoff, maxBackoff) { m_parent = parent; m_child.reset(child); @@ -786,62 +782,51 @@ public: connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); } - virtual ~InstanceStaging() {}; - + virtual ~InstanceStaging(){}; // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { - if(m_child && m_child->canAbort()) - { + if (m_child && m_child->canAbort()) { return m_child->abort(); } return false; } bool canAbort() const override { - if(m_child && m_child->canAbort()) - { + if (m_child && m_child->canAbort()) { return true; } return false; } -protected: - virtual void executeTask() override - { - m_child->start(); - } - QStringList warnings() const override - { - return m_child->warnings(); - } + protected: + virtual void executeTask() override { m_child->start(); } + QStringList warnings() const override { return m_child->warnings(); } -private slots: + private slots: void childSucceded() { unsigned sleepTime = backoff(); - if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) - { + if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) { emitSucceeded(); return; } // we actually failed, retry? - if(sleepTime == maxBackoff) - { + if (sleepTime == maxBackoff) { emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } - void childFailed(const QString & reason) + void childFailed(const QString& reason) { m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } -private: + private: /* * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * Basically, it starts messing things up while the launcher is extracting/creating instances @@ -849,14 +834,14 @@ private: */ ExponentialSeries backoff; QString m_stagingPath; - InstanceList * m_parent; + InstanceList* m_parent; unique_qobject_ptr<Task> m_child; QString m_instanceName; QString m_groupName; QTimer m_backoffTimer; }; -Task * InstanceList::wrapInstanceTask(InstanceTask * task) +Task* InstanceList::wrapInstanceTask(InstanceTask* task) { auto stagingPath = getStagedInstancePath(); task->setStagingPath(stagingPath); @@ -871,8 +856,7 @@ QString InstanceList::getStagedInstancePath() QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); auto path = FS::PathCombine(m_instDir, relPath); - if(!rootPath.mkpath(relPath)) - { + if (!rootPath.mkpath(relPath)) { return QString(); } #ifdef Q_OS_WIN32 @@ -889,8 +873,7 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); - if(!dir.rename(path, destination)) - { + if (!dir.rename(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } @@ -909,7 +892,8 @@ bool InstanceList::destroyStagingPath(const QString& keyPath) return FS::deletePath(keyPath); } -int InstanceList::getTotalPlayTime() { +int InstanceList::getTotalPlayTime() +{ updateTotalPlayTime(); return totalPlayTime; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index bc6c3af0..62282f04 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -19,6 +19,8 @@ #include <QAbstractListModel> #include <QSet> #include <QList> +#include <QStack> +#include <QPair> #include "BaseInstance.h" @@ -46,6 +48,12 @@ enum class GroupsState Dirty }; +struct TrashHistoryItem { + QString id; + QString polyPath; + QString trashPath; + QString groupName; +}; class InstanceList : public QAbstractListModel { @@ -102,6 +110,9 @@ public: void setInstanceGroup(const InstanceId & id, const GroupId& name); void deleteGroup(const GroupId & name); + bool trashInstance(const InstanceId &id); + bool trashedSomething(); + void undoTrashInstance(); void deleteInstance(const InstanceId & id); // Wrap an instance creation task in some more task machinery and make it ready to be used @@ -180,4 +191,6 @@ private: QSet<InstanceId> instanceSet; bool m_groupsLoaded = false; bool m_instancesProbed = false; + + QStack<TrashHistoryItem> m_trashHistory; }; diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index ae6cd247..aa4d1123 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.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 "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( @@ -19,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."); @@ -69,7 +105,7 @@ void JavaCommon::javaBinaryWasBad(QWidget *parent, JavaCheckResult result) void JavaCommon::javaCheckNotFound(QWidget *parent) { QString text; - text += QObject::tr("Java checker library could not be found. Please check your installation"); + 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(); } 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 d36ee3fe..11f9b2bb 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -145,16 +145,26 @@ void LaunchController::login() { return; } - // we try empty password first :) - QString password; // we loop until the user succeeds in logging in or gives up bool tryagain = true; - // the failure. the default failure. - const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change."); - QString failReason = needLoginAgain; + unsigned int tries = 0; while (tryagain) { + if (tries > 0 && tries % 3 == 0) { + auto result = QMessageBox::question( + m_parentWidget, + tr("Continue launch?"), + tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?") + .arg(tries) + ); + + if (result == QMessageBox::No) { + emitAborted(); + return; + } + } + tries++; m_session = std::make_shared<AuthSession>(); m_session->wants_online = m_online; m_accountToUse->fillSession(m_session); diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 528e360e..68fac26a 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -18,13 +18,17 @@ LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" -# Set up env - filter out input LD_ variables but pass them in under different names -export GAME_LIBRARY_PATH=${GAME_LIBRARY_PATH-${LD_LIBRARY_PATH}} -export GAME_PRELOAD=${GAME_PRELOAD-${LD_PRELOAD}} -export LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@":$LAUNCHER_LIBRARY_PATH -export LD_PRELOAD=$LAUNCHER_PRELOAD -export QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" -export QT_FONTPATH="${LAUNCHER_DIR}/fonts" +# Set up env. +# Pass our custom variables separately so that the launcher can remove them for child processes +export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@" +export LAUNCHER_LD_PRELOAD="" +export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" +export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts" + +export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH" +export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD" +export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH" +export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH" # Detect missing dependencies... DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'` 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 d7ad4428..04ca5094 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -127,7 +127,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList } // ours -bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods) +bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods) { QuaZip zipOut(targetJarPath); if (!zipOut.open(QuaZip::mdCreate)) @@ -141,42 +141,41 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QSet<QString> addedFiles; // Modify the jar - QListIterator<Mod> i(mods); - i.toBack(); - while (i.hasPrevious()) + // This needs to be done in reverse-order to ensure we respect the loading order of components + for (auto i = mods.crbegin(); i != mods.crend(); i++) { - const Mod &mod = i.previous(); + const auto* mod = *i; // do not merge disabled mods. - if (!mod.enabled()) + if (!mod->enabled()) continue; - if (mod.type() == Mod::MOD_ZIPFILE) + if (mod->type() == Mod::MOD_ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles)) + if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } } - else if (mod.type() == Mod::MOD_SINGLEFILE) + else if (mod->type() == Mod::MOD_SINGLEFILE) { // FIXME: buggy - does not work with addedFiles - auto filename = mod.fileinfo(); + auto filename = mod->fileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); } - else if (mod.type() == Mod::MOD_FOLDER) + else if (mod->type() == Mod::MOD_FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles - auto filename = mod.fileinfo(); + auto filename = mod->fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); @@ -193,7 +192,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder " << filename.fileName() << " from " @@ -204,7 +203,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.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; return false; } } @@ -269,7 +268,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re // ours -nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) { QDir directory(target); QStringList extracted; @@ -278,7 +277,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & auto numEntries = zip->getEntriesCount(); if(numEntries < 0) { qWarning() << "Failed to enumerate files in archive"; - return nonstd::nullopt; + return std::nullopt; } else if(numEntries == 0) { qDebug() << "Extracting empty archives seems odd..."; @@ -287,7 +286,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & else if (!zip->goToFirstFile()) { qWarning() << "Failed to seek to first file in zip"; - return nonstd::nullopt; + return std::nullopt; } do @@ -324,7 +323,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & { qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; JlCompress::removeFile(extracted); - return nonstd::nullopt; + return std::nullopt; } extracted.append(absFilePath); @@ -342,7 +341,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar } // ours -nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) +std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) @@ -353,13 +352,13 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString return QStringList(); } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + return std::nullopt; } return MMCZip::extractSubDir(&zip, "", dir); } // ours -nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) @@ -370,7 +369,7 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString return QStringList(); } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + return std::nullopt; } return MMCZip::extractSubDir(&zip, subdir, dir); } @@ -421,7 +420,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/MMCZip.h b/launcher/MMCZip.h index bf90cd0b..ce9775bd 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -42,7 +42,7 @@ #include <functional> #include <quazip/JlCompress.h> -#include <nonstd/optional> +#include <optional> namespace MMCZip { @@ -75,7 +75,7 @@ namespace MMCZip /** * take a source jar, add mods to it, resulting in target jar */ - bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods); + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods); /** * Find a single file in archive by file name (not path) @@ -95,7 +95,7 @@ namespace MMCZip /** * Extract a subdirectory from an archive */ - nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); @@ -106,7 +106,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir); + std::optional<QStringList> extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive @@ -116,7 +116,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir); + std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 41856fb5..2b0343f4 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -27,6 +27,7 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde { if (is_indexed) { m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); + connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod); addTask(m_update_task); } @@ -40,12 +41,16 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed); addTask(m_filesNetJob); - } void ModDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); + auto name = std::get<0>(to_delete); + auto filename = std::get<1>(to_delete); + if (!name.isEmpty() && filename != m_mod_version.fileName) { + mods->uninstallMod(filename, true); + } } void ModDownloadTask::downloadFailed(QString reason) @@ -58,3 +63,10 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } + +// This indirection is done so that we don't delete a mod before being sure it was +// downloaded successfully! +void ModDownloadTask::hasOldMod(QString name, QString filename) +{ + to_delete = {name, filename}; +} diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index b3c25909..95020470 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -30,7 +30,7 @@ class ModFolderModel; class ModDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed); + explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true); const QString& getFilename() const { return m_mod_version.fileName; } private: @@ -46,6 +46,11 @@ private: void downloadFailed(QString reason); void downloadSucceeded(); + + std::tuple<QString, QString> to_delete {"", ""}; + +private slots: + void hasOldMod(QString name, QString filename); }; diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index 57974939..173dc5e7 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -77,10 +77,12 @@ public: { return m_ptr; } - bool operator==(const shared_qobject_ptr<T>& other) { + template<typename U> + bool operator==(const shared_qobject_ptr<U>& other) const { return m_ptr == other.m_ptr; } - bool operator!=(const shared_qobject_ptr<T>& other) { + template<typename U> + bool operator!=(const shared_qobject_ptr<U>& other) const { return m_ptr != other.m_ptr; } 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 d426aa80..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" @@ -86,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) { @@ -94,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; 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 15b22260..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> @@ -53,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())); @@ -105,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(); @@ -114,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 24a1556e..2b19fca0 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> @@ -32,25 +52,24 @@ JavaUtils::JavaUtils() { } -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) -static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH) +QString stripVariableEntries(QString name, QString target, QString remove) { - QDir mmcBin(QCoreApplication::applicationDirPath()); - auto items = LD_LIBRARY_PATH.split(':'); - QStringList final; - for(auto & item: items) - { - QDir test(item); - if(test == mmcBin) - { - qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item; - continue; - } - final.append(item); + char delimiter = ':'; +#ifdef Q_OS_WIN32 + delimiter = ';'; +#endif + + auto targetItems = target.split(delimiter); + auto toRemove = remove.split(delimiter); + + for (QString item : toRemove) { + bool removed = targetItems.removeOne(item); + if (!removed) + qWarning() << "Entry" << item + << "could not be stripped from variable" << name; } - return final.join(':'); + return targetItems.join(delimiter); } -#endif QProcessEnvironment CleanEnviroment() { @@ -69,6 +88,16 @@ QProcessEnvironment CleanEnviroment() "JAVA_OPTIONS", "JAVA_TOOL_OPTIONS" }; + + QStringList stripped = + { +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + "LD_LIBRARY_PATH", + "LD_PRELOAD", +#endif + "QT_PLUGIN_PATH", + "QT_FONTPATH" + }; for(auto key: rawenv.keys()) { auto value = rawenv.value(key); @@ -78,19 +107,22 @@ QProcessEnvironment CleanEnviroment() qDebug() << "Env: ignoring" << key << value; continue; } - // filter PolyMC-related things - if(key.startsWith("QT_")) + + // These are used to strip the original variables + // If there is "LD_LIBRARY_PATH" and "LAUNCHER_LD_LIBRARY_PATH", we want to + // remove all values in "LAUNCHER_LD_LIBRARY_PATH" from "LD_LIBRARY_PATH" + if(key.startsWith("LAUNCHER_")) { qDebug() << "Env: ignoring" << key << value; continue; } -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - // Do not pass LD_* variables to java. They were intended for PolyMC - if(key.startsWith("LD_")) + if(stripped.contains(key)) { - qDebug() << "Env: ignoring" << key << value; - continue; + QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key)); + + qDebug() << "Env: stripped" << key << value << "to" << newValue; } +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) // Strip IBus // IBus is a Linux IME framework. For some reason, it breaks MC? if (key == "XMODIFIERS" && value.contains(IBUS)) @@ -99,22 +131,12 @@ QProcessEnvironment CleanEnviroment() value.replace(IBUS, ""); qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; } - if(key == "GAME_PRELOAD") - { - env.insert("LD_PRELOAD", value); - continue; - } - if(key == "GAME_LIBRARY_PATH") - { - env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value)); - continue; - } #endif // qDebug() << "Env: " << key << value; env.insert(key, value); } #ifdef Q_OS_LINUX - // HACK: Workaround for QTBUG42500 + // HACK: Workaround for QTBUG-42500 if(!env.contains("LD_LIBRARY_PATH")) { env.insert("LD_LIBRARY_PATH", ""); @@ -177,25 +199,17 @@ 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) == - ERROR_MORE_DATA) - { - value = new char[valueSz]; - RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); - } - TCHAR subKeyName[255]; + WCHAR 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 @@ -204,34 +218,37 @@ 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) + DWORD valueSz = 0; + if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, + &valueSz) == ERROR_SUCCESS) { - value = new char[valueSz]; - RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, - &valueSz); + WCHAR *value = new WCHAR[valueSz]; + RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value, + &valueSz); + + QString newValue = QString::fromWCharArray(value); + delete [] value; // 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"); + QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe"); javas.append(javaVersion); } diff --git a/launcher/java/JavaUtils.h b/launcher/java/JavaUtils.h index 26d8003b..9b69b516 100644 --- a/launcher/java/JavaUtils.h +++ b/launcher/java/JavaUtils.h @@ -24,6 +24,7 @@ #include <windows.h> #endif +QString stripVariableEntries(QString name, QString target, QString remove); QProcessEnvironment CleanEnviroment(); class JavaUtils : public QObject diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index d5442a2b..28fcc4f4 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -282,18 +282,22 @@ void LaunchTask::emitFailed(QString reason) Task::emitFailed(reason); } -QString LaunchTask::substituteVariables(const QString &cmd) const +void LaunchTask::substituteVariables(QStringList &args) const { - QString out = cmd; - auto variables = m_instance->getVariables(); - for (auto it = variables.begin(); it != variables.end(); ++it) + auto env = m_instance->createEnvironment(); + + for (auto key : env.keys()) { - out.replace("$" + it.key(), it.value()); + args.replaceInStrings("$" + key, env.value(key)); } - auto env = QProcessEnvironment::systemEnvironment(); - for (auto var : env.keys()) +} + +void LaunchTask::substituteVariables(QString &cmd) const +{ + auto env = m_instance->createEnvironment(); + + for (auto key : env.keys()) { - out.replace("$" + var, env.value(var)); + cmd.replace("$" + key, env.value(key)); } - return out; } diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index a1e891ac..9c72b23f 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,7 +105,8 @@ public: /* methods */ shared_qobject_ptr<LogModel> getLogModel(); public: - QString substituteVariables(const QString &cmd) const; + void substituteVariables(QStringList &args) const; + void substituteVariables(QString &cmd) const; QString censorPrivateInfo(QString in); protected: /* methods */ diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 143eb441..ccf07f22 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,20 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask *parent) : LaunchStep(parent) void PostLaunchCommand::executeTask() { - 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); + //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 + m_parent->substituteVariables(m_command); + + emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher); + m_process.start(m_command); +#endif } void PostLaunchCommand::on_state(LoggedProcess::State state) diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 1a0889c8..0b0392cb 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,19 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask *parent) : LaunchStep(parent) void PreLaunchCommand::executeTask() { //FIXME: where to put this? - 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); +#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 + m_parent->substituteVariables(m_command); + + emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher); + m_process.start(m_command); +#endif } void PreLaunchCommand::on_state(LoggedProcess::State state) diff --git a/launcher/main.cpp b/launcher/main.cpp index 3d25b4ff..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,8 +59,10 @@ 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); +#endif // initialize Qt Application app(argc, argv); 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/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 889c6dde..5a6f8de0 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -473,25 +473,25 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() 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; } @@ -540,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); @@ -696,24 +700,24 @@ 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.fileinfo().completeBaseName(); - auto bName = b.fileinfo().completeBaseName(); + std::sort(modList.begin(), modList.end(), [](Mod::Ptr a, Mod::Ptr b) { + auto aName = a->fileinfo().completeBaseName(); + auto bName = b->fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); - for(auto & mod: modList) + for(auto mod: modList) { - if(mod.type() == Mod::MOD_FOLDER) + if(mod->type() == Mod::MOD_FOLDER) { - out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)"; + out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; continue; } - if(mod.enabled()) { - out << u8" [✔️] " + mod.fileinfo().completeBaseName(); + if(mod->enabled()) { + out << u8" [✔] " + mod->fileinfo().completeBaseName(); } else { - out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)"; + out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } @@ -1132,16 +1136,16 @@ std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const return m_game_options; } -QList< Mod > MinecraftInstance::getJarMods() const +QList<Mod*> MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); - QList<Mod> mods; + QList<Mod*> mods; for (auto jarmod : profile->getJarMods()) { QStringList jar, temp1, temp2, temp3; jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); - mods.push_back(Mod(QFileInfo(jar[0]))); + mods.push_back(new Mod(QFileInfo(jar[0]))); } return mods; } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 05450d41..8e1c67f2 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -81,7 +81,7 @@ public: shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; QStringList extraArguments() const override; QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; - QList<Mod> getJarMods() const; + QList<Mod*> getJarMods() const; QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); /// get arguments passed to java QStringList javaArguments() const; 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 01d42b00..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> @@ -688,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(); 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 f242fbe7..a9a0f7f4 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -89,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..90fcf337 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> @@ -33,12 +53,12 @@ #include <QCoreApplication> -#include <nonstd/optional> +#include <optional> -using nonstd::optional; -using nonstd::nullopt; +using std::optional; +using std::nullopt; -GameType::GameType(nonstd::optional<int> original): +GameType::GameType(std::optional<int> original): original(original) { if(!original) { @@ -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/World.h b/launcher/minecraft/World.h index 0f587620..8327253a 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -16,11 +16,11 @@ #pragma once #include <QFileInfo> #include <QDateTime> -#include <nonstd/optional> +#include <optional> struct GameType { GameType() = default; - GameType (nonstd::optional<int> original); + GameType (std::optional<int> original); QString toTranslatedString() const; QString toLogString() const; @@ -33,7 +33,7 @@ struct GameType { Adventure, Spectator } type = Unknown; - nonstd::optional<int> original; + std::optional<int> original; }; class World diff --git a/launcher/minecraft/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 3c7b193c..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 { diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 2b851e18..b3b57c74 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -109,8 +109,10 @@ QStringList AccountList::profileNames() const { void AccountList::addAccount(const MinecraftAccountPtr account) { - // NOTE: Do not allow adding something that's already there - if(m_accounts.contains(account)) { + // NOTE: Do not allow adding something that's already there. We shouldn't let it continue + // because of the signal / slot connections after this. + if (m_accounts.contains(account)) { + qDebug() << "Tried to add account that's already on the accounts list!"; return; } @@ -123,6 +125,8 @@ void AccountList::addAccount(const MinecraftAccountPtr account) if(profileId.size()) { auto existingAccount = findAccountByProfileId(profileId); if(existingAccount != -1) { + qDebug() << "Replacing old account with a new one with the same profile ID!"; + MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; m_accounts[existingAccount] = account; if(m_defaultAccount == existingAccountPtr) { @@ -138,9 +142,12 @@ void AccountList::addAccount(const MinecraftAccountPtr account) // if we don't have this profileId yet, add the account to the end int row = m_accounts.count(); + qDebug() << "Inserting account at index" << row; + beginInsertRows(QModelIndex(), row, row); m_accounts.append(account); endInsertRows(); + onListChanged(); } 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 9c8eb70b..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; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index f5697223..8c53f037 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -5,6 +5,7 @@ #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "minecraft/auth/AccountTask.h" +#include "net/NetUtils.h" LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { @@ -58,10 +59,18 @@ void LauncherLoginStep::onRequestDone( #ifndef NDEBUG qDebug() << data; #endif - emit finished( - AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_) - ); + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_) + ); + } return; } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index add91659..b39b9326 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -4,6 +4,7 @@ #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) { @@ -64,10 +65,18 @@ void MinecraftProfileStep::onRequestDone( qWarning() << " Response:"; qWarning() << QString::fromUtf8(data); - emit finished( - AccountTaskState::STATE_FAILED_SOFT, - tr("Minecraft Java profile acquisition failed.") - ); + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } return; } if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp index d3035272..6a1eb7a0 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -4,6 +4,7 @@ #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) { @@ -67,10 +68,18 @@ void MinecraftProfileStepMojang::onRequestDone( qWarning() << " Response:"; qWarning() << QString::fromUtf8(data); - emit finished( - AccountTaskState::STATE_FAILED_SOFT, - tr("Minecraft Java profile acquisition failed.") - ); + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } return; } if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 589768e3..14bde47e 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -6,6 +6,7 @@ #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind): AuthStep(data), @@ -62,10 +63,24 @@ void XboxAuthorizationStep::onRequestDone( #endif if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; - if(!processSTSError(error, data, headers)) { + if (Net::isApplicationError(error)) { + if(!processSTSError(error, data, headers)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error) + ); + } + else { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_) + ); + } + } + else { emit finished( - AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error) + AccountTaskState::STATE_OFFLINE, + tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_) ); } return; diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp index 9f50138e..738fe1db 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -6,6 +6,7 @@ #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) { @@ -58,10 +59,18 @@ void XboxProfileStep::onRequestDone( #ifndef NDEBUG qDebug() << data; #endif - finished( - AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to retrieve the Xbox profile.") - ); + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_) + ); + } return; } diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index a38a28e4..53069597 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -4,6 +4,7 @@ #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) { @@ -53,7 +54,17 @@ void XboxUserStep::onRequestDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed.")); + if (Net::isApplicationError(error)) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, + tr("XBox user authentication failed: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("XBox user authentication failed: %1").arg(requestor->errorString_) + ); + } return; } diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 56962818..39723b49 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -37,9 +37,9 @@ class Metadata { return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); } - static auto create(QDir& index_dir, Mod& internal_mod) -> ModStruct + static auto create(QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct { - return Packwiz::V1::createModFormat(index_dir, internal_mod); + return Packwiz::V1::createModFormat(index_dir, internal_mod, mod_slug); } static void update(QDir& index_dir, ModStruct& mod) @@ -47,13 +47,23 @@ class Metadata { Packwiz::V1::updateModIndex(index_dir, mod); } - static void remove(QDir& index_dir, QString& mod_name) + static void remove(QDir& index_dir, QString mod_slug) { - Packwiz::V1::deleteModIndex(index_dir, mod_name); + Packwiz::V1::deleteModIndex(index_dir, mod_slug); } - static auto get(QDir& index_dir, QString& mod_name) -> ModStruct + static void remove(QDir& index_dir, QVariant& mod_id) { - return Packwiz::V1::getIndexForMod(index_dir, mod_name); + Packwiz::V1::deleteModIndex(index_dir, mod_id); + } + + static auto get(QDir& index_dir, QString mod_slug) -> ModStruct + { + return Packwiz::V1::getIndexForMod(index_dir, mod_slug); + } + + static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct + { + return Packwiz::V1::getIndexForMod(index_dir, mod_id); } }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 742709e3..588d76e3 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -150,25 +150,30 @@ void Mod::setStatus(ModStatus status) m_temp_status = status; } } -void Mod::setMetadata(Metadata::ModStruct* metadata) +void Mod::setMetadata(const Metadata::ModStruct& metadata) { if (status() == ModStatus::NoMetadata) setStatus(ModStatus::Installed); if (m_localDetails) { - m_localDetails->metadata.reset(metadata); + m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata)); } else { - m_temp_metadata.reset(metadata); + m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata)); } } -auto Mod::destroy(QDir& index_dir) -> bool +auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool { - 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); + if (!preserve_metadata) { + qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); + + if (metadata()) { + Metadata::remove(index_dir, metadata()->slug); + } else { + auto n = name(); + Metadata::remove(index_dir, n); + } + } m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); @@ -182,9 +187,12 @@ auto Mod::details() const -> const ModDetails& auto Mod::name() const -> QString { auto d_name = details().name; - if (!d_name.isEmpty()) { + if (!d_name.isEmpty()) return d_name; - } + + if (metadata()) + return metadata()->name; + return m_name; } @@ -235,11 +243,10 @@ void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details) m_resolved = true; m_localDetails = details; + setStatus(m_temp_status); + if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { - m_localDetails->metadata = m_temp_metadata; - if (status() == ModStatus::NoMetadata) - setStatus(ModStatus::Installed); + setMetadata(*m_temp_metadata); + m_temp_metadata.reset(); } - - setStatus(m_temp_status); } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 5f9c4684..7a13e44b 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -39,10 +39,12 @@ #include <QFileInfo> #include <QList> +#include "QObjectPtr.h" #include "ModDetails.h" -class Mod +class Mod : public QObject { + Q_OBJECT public: enum ModType { @@ -53,6 +55,8 @@ public: MOD_LITEMOD, //!< The mod is a litemod }; + using Ptr = shared_qobject_ptr<Mod>; + Mod() = default; Mod(const QFileInfo &file); explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); @@ -77,12 +81,12 @@ public: auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>; void setStatus(ModStatus status); - void setMetadata(Metadata::ModStruct* metadata); + void setMetadata(const Metadata::ModStruct& metadata); auto enable(bool value) -> bool; // delete all the files of this mod - auto destroy(QDir& index_dir) -> bool; + auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); @@ -111,7 +115,7 @@ protected: 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; + ModStatus m_temp_status = ModStatus::NoMetadata; std::shared_ptr<ModDetails> m_localDetails; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index bc2362a9..112d219e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -65,15 +65,21 @@ void ModFolderModel::startWatching() update(); + // Watch the mods folder is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) - { + if (is_watching) { qDebug() << "Started watching " << m_dir.absolutePath(); - } - else - { + } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); } + + // Watch the mods index folder + is_watching = m_watcher->addPath(indexDir().absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << indexDir().absolutePath(); + } else { + qDebug() << "Failed to start watching " << indexDir().absolutePath(); + } } void ModFolderModel::stopWatching() @@ -82,14 +88,18 @@ void ModFolderModel::stopWatching() return; is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) - { + if (!is_watching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); - } - else - { + } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); } + + is_watching = !m_watcher->removePath(indexDir().absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << indexDir().absolutePath(); + } else { + qDebug() << "Failed to stop watching " << indexDir().absolutePath(); + } } bool ModFolderModel::update() @@ -116,27 +126,36 @@ bool ModFolderModel::update() void ModFolderModel::finishUpdate() { - QSet<QString> currentSet = modsIndex.keys().toSet(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto currentList = modsIndex.keys(); + QSet<QString> currentSet(currentList.begin(), currentList.end()); auto & newMods = m_update->mods; + auto newList = newMods.keys(); + QSet<QString> newSet(newList.begin(), newList.end()); +#else + QSet<QString> currentSet = modsIndex.keys().toSet(); + auto& newMods = m_update->mods; QSet<QString> newSet = newMods.keys().toSet(); +#endif // see if the kept mods changed in some way { QSet<QString> kept = currentSet; kept.intersect(newSet); - for(auto & keptMod: kept) { - auto & newMod = newMods[keptMod]; + for(auto& keptMod : kept) { + auto newMod = newMods[keptMod]; auto row = modsIndex[keptMod]; - auto & currentMod = mods[row]; - if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) { + auto currentMod = mods[row]; + if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) { // no significant change, ignore... continue; } - auto & oldMod = mods[row]; - if(oldMod.isResolving()) { - activeTickets.remove(oldMod.resolutionTicket()); + auto oldMod = mods[row]; + if(oldMod->isResolving()) { + activeTickets.remove(oldMod->resolutionTicket()); } - oldMod = newMod; + + mods[row] = newMod; resolveMod(mods[row]); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -155,9 +174,10 @@ void ModFolderModel::finishUpdate() int removedIndex = *iter; beginRemoveRows(QModelIndex(), removedIndex, removedIndex); auto removedIter = mods.begin() + removedIndex; - if(removedIter->isResolving()) { - activeTickets.remove(removedIter->resolutionTicket()); + if((*removedIter)->isResolving()) { + activeTickets.remove((*removedIter)->resolutionTicket()); } + mods.erase(removedIter); endRemoveRows(); } @@ -183,8 +203,8 @@ void ModFolderModel::finishUpdate() { modsIndex.clear(); int idx = 0; - for(auto & mod: mods) { - modsIndex[mod.internal_id()] = idx; + for(auto mod: mods) { + modsIndex[mod->internal_id()] = idx; idx++; } } @@ -199,17 +219,17 @@ void ModFolderModel::finishUpdate() } } -void ModFolderModel::resolveMod(Mod& m) +void ModFolderModel::resolveMod(Mod::Ptr m) { - if(!m.shouldResolve()) { + if(!m->shouldResolve()) { return; } - auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo()); + auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo()); auto result = task->result(); - result->id = m.internal_id(); + result->id = m->internal_id(); activeTickets.insert(nextResolutionTicket, result); - m.setResolving(true, nextResolutionTicket); + m->setResolving(true, nextResolutionTicket); nextResolutionTicket++; QThreadPool *threadPool = QThreadPool::globalInstance(); connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse); @@ -225,8 +245,8 @@ void ModFolderModel::finishModParse(int token) auto result = *iter; activeTickets.remove(token); int row = modsIndex[result->id]; - auto & mod = mods[row]; - mod.finishResolvingWithDetails(result->details); + auto mod = mods[row]; + mod->finishResolvingWithDetails(result->details); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } @@ -251,6 +271,18 @@ bool ModFolderModel::isValid() return m_dir.exists() && m_dir.isReadable(); } +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr> +{ + QList<Mod::Ptr> selected_mods; + for (auto i : indexes) { + if(i.column() != 0) + continue; + + selected_mods.push_back(mods[i.row()]); + } + return selected_mods; +} + // FIXME: this does not take disabled mod (with extra .disable extension) into account... bool ModFolderModel::installMod(const QString &filename) { @@ -309,7 +341,8 @@ bool ModFolderModel::installMod(const QString &filename) return false; } FS::updateTimestamp(newpath); - installedMod.repath(newpath); + QFileInfo newpathInfo(newpath); + installedMod.repath(newpathInfo); update(); return true; } @@ -327,13 +360,28 @@ 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; } return false; } +bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) +{ + + for(auto mod : allMods()){ + if(mod->fileinfo().fileName() == filename){ + auto index_dir = indexDir(); + mod->destroy(index_dir, preserve_metadata); + return true; + } + } + + return false; +} + bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) { if(interaction_disabled) { @@ -367,9 +415,9 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) if(i.column() != 0) { continue; } - Mod &m = mods[i.row()]; + auto m = mods[i.row()]; auto index_dir = indexDir(); - m.destroy(index_dir); + m->destroy(index_dir); } return true; } @@ -396,9 +444,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const switch (column) { case NameColumn: - return mods[row].name(); + return mods[row]->name(); case VersionColumn: { - switch(mods[row].type()) { + switch(mods[row]->type()) { case Mod::MOD_FOLDER: return tr("Folder"); case Mod::MOD_SINGLEFILE: @@ -406,23 +454,23 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const default: break; } - return mods[row].version(); + return mods[row]->version(); } case DateColumn: - return mods[row].dateTimeChanged(); + return mods[row]->dateTimeChanged(); default: return QVariant(); } case Qt::ToolTipRole: - return mods[row].internal_id(); + return mods[row]->internal_id(); case Qt::CheckStateRole: switch (column) { case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked; default: return QVariant(); } @@ -462,20 +510,20 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio break; case Toggle: default: - desiredStatus = !mod.enabled(); + desiredStatus = !mod->enabled(); break; } - if(desiredStatus == mod.enabled()) { + if(desiredStatus == mod->enabled()) { return true; } // preserve the row, but change its ID - auto oldId = mod.internal_id(); - if(!mod.enable(!mod.enabled())) { + auto oldId = mod->internal_id(); + if(!mod->enable(!mod->enabled())) { return false; } - auto newId = mod.internal_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 24b4d358..a7d3ece0 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -101,13 +101,13 @@ public: { return size() == 0; } - Mod &operator[](size_t index) + Mod& operator[](size_t index) { - return mods[index]; + return *mods[index]; } - const Mod &at(size_t index) const + const Mod& at(size_t index) const { - return mods.at(index); + return *mods.at(index); } /// Reloads the mod list and returns true if the list changed. @@ -118,6 +118,8 @@ public: */ bool installMod(const QString& filename); + bool uninstallMod(const QString& filename, bool preserve_metadata = false); + /// Deletes all the selected mods bool deleteMods(const QModelIndexList &indexes); @@ -139,11 +141,13 @@ public: return { QString("%1/.index").arg(dir().absolutePath()) }; } - const QList<Mod> & allMods() + const QList<Mod::Ptr>& allMods() { return mods; } + auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>; + public slots: void disableInteraction(bool disabled); @@ -157,7 +161,7 @@ signals: void updateFinished(); private: - void resolveMod(Mod& m); + void resolveMod(Mod::Ptr m); bool setModStatus(int index, ModStatusAction action); protected: @@ -171,5 +175,5 @@ protected: QMap<QString, int> modsIndex; QMap<int, LocalModParseTask::ResultPtr> activeTickets; int nextResolutionTicket = 0; - QList<Mod> mods; + QList<Mod::Ptr> mods; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 3354732b..1519f49d 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -4,6 +4,7 @@ #include <QJsonObject> #include <QJsonArray> #include <QJsonValue> +#include <QString> #include <quazip/quazip.h> #include <quazip/quazipfile.h> #include <toml.h> @@ -71,7 +72,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) if(val.isUndefined()) { val = jsonDoc.object().value("modListVersion"); } - int version = val.toDouble(); + + int version = Json::ensureInteger(val, -1); + + // Some mods set the number with "", so it's a String instead + if (version < 0) + version = Json::ensureString(val, "").toInt(); + if (version != 2) { qCritical() << "BAD stuff happened to mod json:"; diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 1bdecb8c..4b878918 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -44,10 +44,21 @@ 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); + auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId); + if (old_metadata.isValid()) { + emit hasOldMod(old_metadata.name, old_metadata.filename); + if (m_mod.slug.isEmpty()) + m_mod.slug = old_metadata.slug; + } - emitSucceeded(); + auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); + if (pw_mod.isValid()) { + Metadata::update(m_index_dir, pw_mod); + emitSucceeded(); + } else { + qCritical() << "Tried to update an invalid mod!"; + emitFailed(tr("Invalid metadata")); + } } auto LocalModUpdateTask::abort() -> bool diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index 2db183e0..1d2f06a6 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -37,6 +37,9 @@ class LocalModUpdateTask : public Task { //! Entry point for tasks. void executeTask() override; + signals: + void hasOldMod(QString name, QString filename); + private: QDir m_index_dir; ModPlatform::IndexedPack& m_mod; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 276414e4..9b70e7a1 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -36,7 +36,6 @@ #include "ModFolderLoadTask.h" -#include "Application.h" #include "minecraft/mod/MetadataHandler.h" ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed) @@ -53,37 +52,48 @@ void ModFolderLoadTask::run() // Read JAR files that don't have metadata m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { - Mod mod(entry); + Mod::Ptr mod(new Mod(entry)); - if (mod.enabled()) { - if (m_result->mods.contains(mod.internal_id())) { - m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + 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); + 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); + QString chopped_id = mod->internal_id().chopped(9); if (m_result->mods.contains(chopped_id)) { - m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod->internal_id()] = mod; - auto metadata = m_result->mods[chopped_id].metadata(); + auto metadata = m_result->mods[chopped_id]->metadata(); if (metadata) { - mod.setMetadata(new Metadata::ModStruct(*metadata)); + mod->setMetadata(*metadata); - m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + 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); + m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } } + // Remove orphan metadata to prevent issues + // See https://github.com/PolyMC/PolyMC/issues/996 + QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods); + while (iter.hasNext()) { + auto mod = iter.next().value(); + if (mod->status() == ModStatus::NotInstalled) { + mod->destroy(m_index_dir, false); + iter.remove(); + } + } + emit succeeded(); } @@ -97,8 +107,8 @@ void ModFolderLoadTask::getFromMetadata() return; } - Mod mod(m_mods_dir, metadata); - mod.setStatus(ModStatus::NotInstalled); - m_result->mods[mod.internal_id()] = mod; + auto* mod = new 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 index 088f873e..0b6bb6cc 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -48,7 +48,7 @@ class ModFolderLoadTask : public QObject, public QRunnable Q_OBJECT public: struct Result { - QMap<QString, Mod> mods; + QMap<QString, Mod::Ptr> mods; }; using ResultPtr = std::shared_ptr<Result>; ResultPtr result() const { 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/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h new file mode 100644 index 00000000..91922034 --- /dev/null +++ b/launcher/modplatform/CheckUpdateTask.h @@ -0,0 +1,51 @@ +#pragma once + +#include "minecraft/mod/Mod.h" +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class ModDownloadTask; +class ModFolderModel; + +class CheckUpdateTask : public Task { + Q_OBJECT + + public: + CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder) + : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; + + struct UpdatableMod { + QString name; + QString old_hash; + QString old_version; + QString new_version; + QString changelog; + ModPlatform::Provider provider; + ModDownloadTask* download; + + public: + UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t) + : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) + {} + }; + + auto getUpdatable() -> std::vector<UpdatableMod>&& { return std::move(m_updatable); } + + public slots: + bool abort() override = 0; + + protected slots: + void executeTask() override = 0; + + signals: + void checkFailed(Mod* failed, QString reason, QUrl recover_url = {}); + + protected: + QList<Mod*>& m_mods; + std::list<Version>& m_game_versions; + ModAPI::ModLoaderTypes m_loaders; + std::shared_ptr<ModFolderModel> m_mods_folder; + + std::vector<UpdatableMod> m_updatable; +}; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp new file mode 100644 index 00000000..60c54c4e --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -0,0 +1,534 @@ +#include "EnsureMetadataTask.h" + +#include <MurmurHash2.h> +#include <QDebug> + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" +#include "net/NetJob.h" +#include "tasks/MultipleOptionsTask.h" + +static ModPlatform::ProviderCapabilities ProviderCaps; + +static ModrinthAPI modrinth_api; +static FlameAPI flame_api; + +EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) +{ + auto hash = getHash(mod); + if (hash.isEmpty()) + emitFail(mod); + else + m_mods.insert(hash, mod); +} + +EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov) + : Task(nullptr), m_index_dir(dir), m_provider(prov) +{ + for (auto* mod : mods) { + if (!mod->valid()) { + emitFail(mod); + continue; + } + + auto hash = getHash(mod); + if (hash.isEmpty()) { + emitFail(mod); + continue; + } + + m_mods.insert(hash, mod); + } +} + +QString EnsureMetadataTask::getHash(Mod* mod) +{ + /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */ + QByteArray jar_data; + try { + jar_data = FS::read(mod->fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name()); + qCritical() << QString("Reason: ") << e.cause(); + + return {}; + } + + switch (m_provider) { + case ModPlatform::Provider::MODRINTH: { + auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + + return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex()); + } + case ModPlatform::Provider::FLAME: { + QByteArray jar_data_treated; + for (char c : jar_data) { + // CF-specific + if (!(c == 9 || c == 10 || c == 13 || c == 32)) + jar_data_treated.push_back(c); + } + + return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length())); + } + } + + return {}; +} + +bool EnsureMetadataTask::abort() +{ + // Prevent sending signals to a dead object + disconnect(this, 0, 0, 0); + + if (m_current_task) + return m_current_task->abort(); + return true; +} + +void EnsureMetadataTask::executeTask() +{ + setStatus(tr("Checking if mods have metadata...")); + + for (auto* mod : m_mods) { + if (!mod->valid()) { + qDebug() << "Mod" << mod->name() << "is invalid!"; + emitFail(mod); + continue; + } + + // They already have the right metadata :o + if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) { + qDebug() << "Mod" << mod->name() << "already has metadata!"; + emitReady(mod); + continue; + } + + // Folders don't have metadata + if (mod->type() == Mod::MOD_FOLDER) { + emitReady(mod); + } + } + + NetJob::Ptr version_task; + + switch (m_provider) { + case (ModPlatform::Provider::MODRINTH): + version_task = modrinthVersionsTask(); + break; + case (ModPlatform::Provider::FLAME): + version_task = flameVersionsTask(); + break; + } + + auto invalidade_leftover = [this] { + QMutableHashIterator<QString, Mod*> mods_iter(m_mods); + while (mods_iter.hasNext()) { + auto mod = mods_iter.next(); + emitFail(mod.value()); + } + + emitSucceeded(); + }; + + connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] { + NetJob::Ptr project_task; + + switch (m_provider) { + case (ModPlatform::Provider::MODRINTH): + project_task = modrinthProjectsTask(); + break; + case (ModPlatform::Provider::FLAME): + project_task = flameProjectsTask(); + break; + } + + if (!project_task) { + invalidade_leftover(); + return; + } + + connect(project_task.get(), &Task::finished, this, [=] { + invalidade_leftover(); + project_task->deleteLater(); + m_current_task = nullptr; + }); + + m_current_task = project_task.get(); + project_task->start(); + }); + + connect(version_task.get(), &Task::finished, [=] { + version_task->deleteLater(); + m_current_task = nullptr; + }); + + if (m_mods.size() > 1) + setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider))); + else if (!m_mods.empty()) + setStatus(tr("Requesting metadata information from %1 for '%2'...") + .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name())); + + m_current_task = version_task.get(); + version_task->start(); +} + +void EnsureMetadataTask::emitReady(Mod* m) +{ + qDebug() << QString("Generated metadata for %1").arg(m->name()); + emit metadataReady(m); + + m_mods.remove(getHash(m)); +} + +void EnsureMetadataTask::emitFail(Mod* m) +{ + qDebug() << QString("Failed to generate metadata for %1").arg(m->name()); + emit metadataFailed(m); + + m_mods.remove(getHash(m)); +} + +// Modrinth + +NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() +{ + auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + + auto* response = new QByteArray(); + auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); + + // Prevents unfortunate timings when aborting the task + if (!ver_task) + return {}; + + connect(ver_task.get(), &NetJob::succeeded, this, [this, response] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + try { + auto entries = Json::requireObject(doc); + for (auto& hash : m_mods.keys()) { + auto mod = m_mods.find(hash).value(); + try { + auto entry = Json::requireObject(entries, hash); + + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); + qDebug() << "Getting version for" << mod->name() << "from Modrinth"; + + m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return ver_task; +} + +NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask() +{ + QHash<QString, QString> addonIds; + for (auto const& data : m_temp_versions) + addonIds.insert(data.addonId.toString(), data.hash); + + auto response = new QByteArray(); + NetJob::Ptr proj_task; + + if (addonIds.isEmpty()) { + qWarning() << "No addonId found!"; + } else if (addonIds.size() == 1) { + proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response); + } else { + proj_task = modrinth_api.getProjects(addonIds.keys(), response); + } + + // Prevents unfortunate timings when aborting the task + if (!proj_task) + return {}; + + connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { doc.object() }; + else + entries = Json::requireArray(doc); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + + ModPlatform::IndexedPack pack; + Modrinth::loadIndexedPack(pack, entry_obj); + + auto hash = addonIds.find(pack.addonId.toString()).value(); + + auto mod_iter = m_mods.find(hash); + if (mod_iter == m_mods.end()) { + qWarning() << "Invalid project id from the API response."; + continue; + } + + auto* mod = mod_iter.value(); + + try { + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); + + modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return proj_task; +} + +// Flame +NetJob::Ptr EnsureMetadataTask::flameVersionsTask() +{ + auto* response = new QByteArray(); + + QList<uint> fingerprints; + for (auto& murmur : m_mods.keys()) { + fingerprints.push_back(murmur.toUInt()); + } + + auto ver_task = flame_api.matchFingerprints(fingerprints, response); + + connect(ver_task.get(), &Task::succeeded, this, [this, response] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + auto data_arr = Json::requireArray(data_obj, "exactMatches"); + + if (data_arr.isEmpty()) { + qWarning() << "No matches found for fingerprint search!"; + + return; + } + + for (auto match : data_arr) { + auto match_obj = Json::ensureObject(match, {}); + auto file_obj = Json::ensureObject(match_obj, "file", {}); + + if (match_obj.isEmpty() || file_obj.isEmpty()) { + qWarning() << "Fingerprint match is empty!"; + + return; + } + + auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt()); + auto mod = m_mods.find(fingerprint); + if (mod == m_mods.end()) { + qWarning() << "Invalid fingerprint from the API response."; + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name())); + + m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); + } + + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return ver_task; +} + +NetJob::Ptr EnsureMetadataTask::flameProjectsTask() +{ + QHash<QString, QString> addonIds; + for (auto const& hash : m_mods.keys()) { + if (m_temp_versions.contains(hash)) { + auto const& data = m_temp_versions.find(hash).value(); + + auto id_str = data.addonId.toString(); + if (!id_str.isEmpty()) + addonIds.insert(data.addonId.toString(), hash); + } + } + + auto response = new QByteArray(); + NetJob::Ptr proj_task; + + if (addonIds.isEmpty()) { + qWarning() << "No addonId found!"; + } else if (addonIds.size() == 1) { + proj_task = flame_api.getProject(*addonIds.keyBegin(), response); + } else { + proj_task = flame_api.getProjects(addonIds.keys(), response); + } + + // Prevents unfortunate timings when aborting the task + if (!proj_task) + return {}; + + connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + + auto id = QString::number(Json::requireInteger(entry_obj, "id")); + auto hash = addonIds.find(id).value(); + auto mod = m_mods.find(hash).value(); + + try { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); + + ModPlatform::IndexedPack pack; + FlameMod::loadIndexedPack(pack, entry_obj); + + flameCallback(pack, m_temp_versions.find(hash).value(), mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return proj_task; +} + +void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) +{ + // Prevent file name mismatch + ver.fileName = mod->fileinfo().fileName(); + if (ver.fileName.endsWith(".disabled")) + ver.fileName.chop(9); + + QDir tmp_index_dir(m_index_dir); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + + update_metadata.start(); + + if (!update_metadata.isFinished()) + loop.exec(); + } + + auto metadata = Metadata::get(tmp_index_dir, pack.slug); + if (!metadata.isValid()) { + qCritical() << "Failed to generate metadata at last step!"; + emitFail(mod); + return; + } + + mod->setMetadata(metadata); + + emitReady(mod); +} + +void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) +{ + try { + // Prevent file name mismatch + ver.fileName = mod->fileinfo().fileName(); + if (ver.fileName.endsWith(".disabled")) + ver.fileName.chop(9); + + QDir tmp_index_dir(m_index_dir); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + + update_metadata.start(); + + if (!update_metadata.isFinished()) + loop.exec(); + } + + auto metadata = Metadata::get(tmp_index_dir, pack.slug); + if (!metadata.isValid()) { + qCritical() << "Failed to generate metadata at last step!"; + emitFail(mod); + return; + } + + mod->setMetadata(metadata); + + emitReady(mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + + emitFail(mod); + } +} diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h new file mode 100644 index 00000000..79db6976 --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -0,0 +1,54 @@ +#pragma once + +#include "ModIndex.h" +#include "tasks/SequentialTask.h" +#include "net/NetJob.h" + +class Mod; +class QDir; +class MultipleOptionsTask; + +class EnsureMetadataTask : public Task { + Q_OBJECT + + public: + EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + + ~EnsureMetadataTask() = default; + + public slots: + bool abort() override; + protected slots: + void executeTask() override; + + private: + // FIXME: Move to their own namespace + auto modrinthVersionsTask() -> NetJob::Ptr; + auto modrinthProjectsTask() -> NetJob::Ptr; + + auto flameVersionsTask() -> NetJob::Ptr; + auto flameProjectsTask() -> NetJob::Ptr; + + // Helpers + void emitReady(Mod*); + void emitFail(Mod*); + + auto getHash(Mod*) -> QString; + + private slots: + void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); + void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); + + signals: + void metadataReady(Mod*); + void metadataFailed(Mod*); + + private: + QHash<QString, Mod*> m_mods; + QDir m_index_dir; + ModPlatform::Provider m_provider; + + QHash<QString, ModPlatform::IndexedVersion> m_temp_versions; + NetJob* m_current_task; +}; diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 91b760df..4114d83c 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -1,9 +1,46 @@ +// 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" +#include "net/NetJob.h" namespace ModPlatform { class ListModel; @@ -38,6 +75,9 @@ class ModAPI { virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; + virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0; + virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0; + struct VersionSearchArgs { QString addonId; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index c27643af..dc297d03 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -54,13 +54,16 @@ struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; - QVector<QString> mcVersion; + QString version_number = {}; + QStringList mcVersion; QString downloadUrl; QString date; QString fileName; - QVector<QString> loaders = {}; + QStringList loaders = {}; QString hash_type; QString hash; + bool is_preferred = true; + QString changelog; }; struct ExtraPackData { @@ -76,6 +79,7 @@ struct IndexedPack { QVariant addonId; Provider provider; QString name; + QString slug; QString description; QList<ModpackAuthor> authors; QString logoName; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 01c82baa..5ed13470 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -36,7 +36,7 @@ #include "ATLPackInstallTask.h" -#include <QtConcurrent/QtConcurrent> +#include <QtConcurrent> #include <quazip/quazip.h> @@ -693,7 +693,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(); @@ -838,7 +842,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, [&]() { @@ -890,7 +898,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; @@ -966,14 +974,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()) { diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index d5ecae34..a7124d59 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -46,7 +46,7 @@ #include "minecraft/PackProfile.h" #include "meta/Version.h" -#include <nonstd/optional> +#include <optional> namespace ATLauncher { @@ -139,8 +139,8 @@ private: Meta::VersionPtr minecraftVersion; QMap<QString, Meta::VersionPtr> componentsToInstall; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; QFuture<bool> m_modExtractFuture; QFutureWatcher<bool> m_modExtractFutureWatcher; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index c1f56658..058d2471 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -7,6 +7,13 @@ Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAcc : m_network(network), m_toProcess(toProcess) {} +bool Flame::FileResolvingTask::abort() +{ + if (m_dljob) + return m_dljob->abort(); + return true; +} + void Flame::FileResolvingTask::executeTask() { setStatus(tr("Resolving mod IDs...")); diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index 87981f0a..f71b87ce 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -13,6 +13,9 @@ public: explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess); virtual ~FileResolvingTask() {}; + bool canAbort() const override { return true; } + bool abort() override; + const Flame::Manifest &getResults() const { return m_toProcess; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp new file mode 100644 index 00000000..0ff04f72 --- /dev/null +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -0,0 +1,148 @@ +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include "net/Upload.h" + +auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray fingerprints_arr; + for (auto& fp : fingerprints) { + fingerprints_arr.append(QString("%1").arg(fp)); + } + + body_obj["fingerprints"] = fingerprints_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString +{ + QEventLoop lock; + QString changelog; + + auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network()); + auto* response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray( + QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") + .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), + response)); + + QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + changelog = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob, &NetJob::finished, [response, &lock] { + delete response; + lock.quit(); + }); + + netJob->start(); + lock.exec(); + + return changelog; +} + +auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion +{ + QEventLoop loop; + + auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network()); + auto response = new QByteArray(); + ModPlatform::IndexedVersion ver; + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); + + QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + QJsonObject latest_file_obj; + ModPlatform::IndexedVersion ver_tmp; + + for (auto file : arr) { + auto file_obj = Json::requireObject(file); + auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); + if(file_tmp.date > ver_tmp.date) { + ver_tmp = file_tmp; + latest_file_obj = file_obj; + } + } + + ver = FlameMod::loadIndexedPackVersion(latest_file_obj); + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + }); + + QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] { + netJob->deleteLater(); + delete response; + loop.quit(); + }); + + netJob->start(); + + loop.exec(); + + return ver; +} + +auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* +{ + auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray addons_arr; + for (auto& addonId : addonIds) { + addons_arr.append(addonId); + } + + body_obj["modIds"] = addons_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); + QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + + return netJob; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index aea76ff1..336df387 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,6 +4,14 @@ #include "modplatform/helpers/NetworkModAPI.h" class FlameAPI : public NetworkModAPI { + public: + auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr; + auto getModFileChangelog(int modId, int fileId) -> QString; + + auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; + + auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; + private: inline auto getSortFieldInt(QString sortString) const -> int { diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp new file mode 100644 index 00000000..8dd3a846 --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -0,0 +1,179 @@ +#include "FlameCheckUpdate.h" +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include <MurmurHash2.h> + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static FlameAPI api; + +bool FlameCheckUpdate::abort() +{ + m_was_aborted = true; + if (m_net_job) + return m_net_job->abort(); + return true; +} + +ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) +{ + ModPlatform::IndexedPack pack; + + QEventLoop loop; + + auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); + + auto response = new QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); + auto dl = Net::Download::makeByteArray(url, response); + get_project_job->addNetAction(dl); + + QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + FlameMod::loadIndexedPack(pack, data_obj); + } catch (Json::JsonException& e) { + qWarning() << e.cause(); + qDebug() << doc; + } + }); + + QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] { + get_project_job->deleteLater(); + loop.quit(); + }); + + get_project_job->start(); + loop.exec(); + + return pack; +} + +ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) +{ + ModPlatform::IndexedVersion ver; + + QEventLoop loop; + + auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network()); + + auto response = new QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); + auto dl = Net::Download::makeByteArray(url, response); + get_file_info_job->addNetAction(dl); + + QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + ver = FlameMod::loadIndexedPackVersion(data_obj); + } catch (Json::JsonException& e) { + qWarning() << e.cause(); + qDebug() << doc; + } + }); + + QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] { + get_file_info_job->deleteLater(); + loop.quit(); + }); + + get_file_info_job->start(); + loop.exec(); + + return ver; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void FlameCheckUpdate::executeTask() +{ + setStatus(tr("Preparing mods for CurseForge...")); + + int i = 0; + for (auto* mod : m_mods) { + if (!mod->enabled()) { + emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); + continue; + } + + setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); + setProgress(i++, m_mods.size()); + + auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders }); + + // Check if we were aborted while getting the latest version + if (m_was_aborted) { + aborted(); + return; + } + + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name())); + + if (!latest_ver.addonId.isValid()) { + emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game " + "version / mod loader.")); + continue; + } + + if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) { + auto pack = getProjectInfo(latest_ver); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); + emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url); + + continue; + } + + if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) { + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod->name(); + pack.slug = mod->metadata()->slug; + pack.addonId = mod->metadata()->project_id; + pack.websiteUrl = mod->homeurl(); + for (auto& author : mod->authors()) + pack.authors.append({ author }); + pack.description = mod->description(); + pack.provider = ModPlatform::Provider::FLAME; + + auto old_version = mod->version(); + if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { + auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt()); + old_version = current_ver.version; + } + + auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); + m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version, + api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), + ModPlatform::Provider::FLAME, download_task); + } + } + + emitSucceeded(); +} diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h new file mode 100644 index 00000000..163c706c --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Application.h" +#include "modplatform/CheckUpdateTask.h" +#include "net/NetJob.h" + +class FlameCheckUpdate : public CheckUpdateTask { + Q_OBJECT + + public: + FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder) + : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + + private: + NetJob* m_net_job = nullptr; + + bool m_was_aborted = false; +}; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index b99bfb26..746018e2 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -7,20 +7,22 @@ #include "net/NetJob.h" static ModPlatform::ProviderCapabilities ProviderCaps; +static FlameAPI api; 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.slug = Json::requireString(obj, "slug"); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); - QJsonObject logo = Json::requireObject(obj, "logo"); - pack.logoName = Json::requireString(logo, "title"); - pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); + QJsonObject logo = Json::ensureObject(obj, "logo"); + pack.logoName = Json::ensureString(logo, "title"); + pack.logoUrl = Json::ensureString(logo, "thumbnailUrl"); - auto authors = Json::requireArray(obj, "authors"); + auto authors = Json::ensureArray(obj, "authors"); for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); ModPlatform::ModpackAuthor packAuthor; @@ -91,7 +93,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versionsLoaded = true; } -auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion { auto versionArray = Json::requireArray(obj, "gameVersions"); if (versionArray.isEmpty()) { @@ -110,7 +112,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV 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.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); auto hash_list = Json::ensureArray(obj, "hashes"); @@ -124,5 +126,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV break; } } + + if(load_changelog) + file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); + return file; } diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 9c6c1c6c..a839dd83 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -17,6 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 26a48d1c..677db1c3 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.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 <QString> @@ -25,7 +60,7 @@ struct File bool resolved = false; QString fileName; QUrl url; - QString targetFolder = QLatin1Literal("mods"); + QString targetFolder = QStringLiteral("mods"); enum class Type { Unknown, diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index d7abd10f..90edfe31 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -33,18 +33,14 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const 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)); + auto job = getProject(pack.addonId.toString(), response); - QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] { + QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] { QJsonParseError parse_error{}; - auto doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument 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 + qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; @@ -53,7 +49,7 @@ void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pac caller->infoRequestFinished(doc, pack); }); - netJob->start(); + job->start(); } void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const @@ -83,3 +79,18 @@ void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) co netJob->start(); } + +auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob* +{ + auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + auto searchUrl = getModInfoURL(addonId); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::finished, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + return netJob; +} diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 87d77ad1..989bcec4 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -8,6 +8,8 @@ class NetworkModAPI : public ModAPI { void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; + auto getProject(QString addonId, QByteArray* response) const -> NetJob* override; + protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; virtual auto getModInfoURL(QString& id) 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/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index a7395220..da4c0da5 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -10,7 +10,7 @@ #include "net/NetJob.h" -#include <nonstd/optional> +#include <optional> namespace LegacyFTB { @@ -46,8 +46,8 @@ private: /* data */ shared_qobject_ptr<QNetworkAccessManager> m_network; bool abortable = false; std::unique_ptr<QuaZip> m_packZip; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; NetJob::Ptr netJobContainer; QString archivePath; diff --git a/launcher/modplatform/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 c324ffda..16013070 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,103 +42,190 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "modplatform/flame/PackManifest.h" #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" -#include "BuildConfig.h" #include "Application.h" +#include "BuildConfig.h" +#include "ui/dialogs/ScrollMessageBox.h" namespace ModpacksCH { -PackInstallTask::PackInstallTask(Modpack pack, QString version) -{ - m_pack = pack; - m_version_name = version; -} +PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent) + : m_pack(std::move(pack)), m_version_name(std::move(version)), m_parent(parent) +{} bool PackInstallTask::abort() { - if(abortable) - { - return jobPtr->abort(); - } - return false; + bool aborted = true; + + if (m_net_job) + aborted &= m_net_job->abort(); + if (m_mod_id_resolver_task) + aborted &= m_mod_id_resolver_task->abort(); + + // FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet... + if (aborted) + emitFailed(tr("Aborted")); + + return aborted; } void PackInstallTask::executeTask() { - // Find pack version - bool found = false; - VersionInfo version; + setStatus(tr("Getting the manifest...")); - for(auto vInfo : m_pack.versions) { - if (vInfo.name == m_version_name) { - found = true; - version = vInfo; - break; - } - } + // Find pack version + auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(), + [this](ModpacksCH::VersionInfo const& a) { return a.name == m_version_name; }); - if(!found) { + if (version_it == m_pack.versions.constEnd()) { emitFailed(tr("Failed to find pack version %1").arg(m_version_name)); return; } - auto *netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network()); + auto version = *version_it; + + auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network()); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed); + QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress); - QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); - QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + m_net_job = netJob; + + netJob->start(); } -void PackInstallTask::onDownloadSucceeded() +void PackInstallTask::onManifestDownloadSucceeded() { - jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + m_net_job.reset(); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << m_response; return; } - auto obj = doc.object(); - ModpacksCH::Version version; - try - { + try { + auto obj = Json::requireObject(doc); ModpacksCH::loadVersion(version, obj); - } - catch (const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } + m_version = version; - downloadPack(); + resolveMods(); } -void PackInstallTask::onDownloadFailed(QString reason) +void PackInstallTask::resolveMods() { - jobPtr.reset(); - emitFailed(reason); + setStatus(tr("Resolving mods...")); + setProgress(0, 100); + + m_file_id_map.clear(); + + Flame::Manifest manifest; + int index = 0; + + for (auto const& file : m_version.files) { + if (!file.serverOnly && file.url.isEmpty()) { + if (file.curseforge.file_id <= 0) { + emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name)); + return; + } + + Flame::File flame_file; + flame_file.projectId = file.curseforge.project_id; + flame_file.fileId = file.curseforge.file_id; + flame_file.hash = file.sha1; + + manifest.files.insert(flame_file.fileId, flame_file); + m_file_id_map.append(flame_file.fileId); + } else { + m_file_id_map.append(-1); + } + + index++; + } + + m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest); + + connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded); + connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed); + connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress); + + m_mod_id_resolver_task->start(); +} + +void PackInstallTask::onResolveModsSucceeded() +{ + m_abortable = false; + + QString text; + auto anyBlocked = false; + + Flame::Manifest results = m_mod_id_resolver_task->getResults(); + for (int index = 0; index < m_file_id_map.size(); index++) { + auto const file_id = m_file_id_map.at(index); + if (file_id < 0) + continue; + + Flame::File results_file = results.files[file_id]; + VersionFile& local_file = m_version.files[index]; + + // First check for blocked mods + if (!results_file.resolved || results_file.url.isEmpty()) { + QString type(local_file.type); + + type[0] = type[0].toUpper(); + text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl); + anyBlocked = true; + } else { + local_file.url = results_file.url.toString(); + } + } + + m_mod_id_resolver_task.reset(); + + if (anyBlocked) { + qDebug() << "Blocked files found, displaying file list"; + + auto message_dialog = new ScrollMessageBox(m_parent, tr("Blocked files found"), + tr("The following files are not available for download in third party launchers.<br/>" + "You will need to manually download them and add them to the instance."), + text); + + if (message_dialog->exec() == QDialog::Accepted) + downloadPack(); + else + abort(); + } else { + downloadPack(); + } } void PackInstallTask::downloadPack() { setStatus(tr("Downloading mods...")); - jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); - for(auto file : m_version.files) { - if(file.serverOnly) continue; + auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); + for (auto const& file : m_version.files) { + if (file.serverOnly || file.url.isEmpty()) + continue; - QFileInfo fileName(file.name); - auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix(); + QFileInfo file_info(file.name); + auto cacheName = file_info.completeBaseName() + "-" + file.sha1 + "." + file_info.suffix(); auto entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", cacheName); entry->setStale(true); @@ -144,58 +233,64 @@ void PackInstallTask::downloadPack() auto relpath = FS::PathCombine("minecraft", file.path, file.name); auto path = FS::PathCombine(m_stagingPath, relpath); - if (filesToCopy.contains(path)) { + if (m_files_to_copy.contains(path)) { qWarning() << "Ignoring" << file.url << "as a file of that path is already downloading."; continue; } + qDebug() << "Will download" << file.url << "to" << path; - filesToCopy[path] = entry->getFullPath(); + m_files_to_copy[path] = entry->getFullPath(); auto dl = Net::Download::makeCached(file.url, entry); if (!file.sha1.isEmpty()) { auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); } + jobPtr->addNetAction(dl); } - connect(jobPtr.get(), &NetJob::succeeded, this, [&]() - { - abortable = false; - jobPtr.reset(); - install(); - }); - connect(jobPtr.get(), &NetJob::failed, [&](QString reason) - { - abortable = false; - jobPtr.reset(); - emitFailed(reason); - }); - connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) - { - abortable = true; - setProgress(current, total); - }); + connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded); + connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed); + connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress); + m_net_job = jobPtr; jobPtr->start(); + + m_abortable = true; +} + +void PackInstallTask::onModDownloadSucceeded() +{ + m_net_job.reset(); + install(); } void PackInstallTask::install() { - setStatus(tr("Copying modpack files")); + setStatus(tr("Copying modpack files...")); + setProgress(0, m_files_to_copy.size()); + QCoreApplication::processEvents(); + + m_abortable = false; - for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) { - auto &to = iter.key(); - auto &from = iter.value(); + int i = 0; + for (auto iter = m_files_to_copy.constBegin(); iter != m_files_to_copy.constEnd(); iter++) { + auto& to = iter.key(); + auto& from = iter.value(); FS::copy fileCopyOperation(from, to); - if(!fileCopyOperation()) { + if (!fileCopyOperation()) { qWarning() << "Failed to copy" << from << "to" << to; emitFailed(tr("Failed to copy files")); return; } + + setProgress(i++, m_files_to_copy.size()); + QCoreApplication::processEvents(); } - setStatus(tr("Installing modpack")); + setStatus(tr("Installing modpack...")); + QCoreApplication::processEvents(); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath); @@ -205,21 +300,21 @@ void PackInstallTask::install() auto components = instance.getPackProfile(); components->buildingFromScratch(); - for(auto target : m_version.targets) { - if(target.type == "game" && target.name == "minecraft") { + for (auto target : m_version.targets) { + if (target.type == "game" && target.name == "minecraft") { components->setComponentVersion("net.minecraft", target.version, true); break; } } - for(auto target : m_version.targets) { - if(target.type != "modloader") continue; + for (auto target : m_version.targets) { + if (target.type != "modloader") + continue; - if(target.name == "forge") { - components->setComponentVersion("net.minecraftforge", target.version, true); - } - else if(target.name == "fabric") { - components->setComponentVersion("net.fabricmc.fabric-loader", target.version, true); + if (target.name == "forge") { + components->setComponentVersion("net.minecraftforge", target.version); + } else if (target.name == "fabric") { + components->setComponentVersion("net.fabricmc.fabric-loader", target.version); } } @@ -245,4 +340,20 @@ void PackInstallTask::install() emitSucceeded(); } +void PackInstallTask::onManifestDownloadFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} +void PackInstallTask::onResolveModsFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); } +void PackInstallTask::onModDownloadFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} + +} // namespace ModpacksCH diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h index ff59b695..e63ca0df 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -1,18 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -20,44 +40,60 @@ #include "FTBPackManifest.h" #include "InstanceTask.h" +#include "QObjectPtr.h" +#include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" +#include <QWidget> + namespace ModpacksCH { -class PackInstallTask : public InstanceTask +class PackInstallTask final : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(Modpack pack, QString version); - virtual ~PackInstallTask(){} + explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr); + ~PackInstallTask() override = default; - bool canAbort() const override { return true; } + bool canAbort() const override { return m_abortable; } bool abort() override; protected: - virtual void executeTask() override; + void executeTask() override; private slots: - void onDownloadSucceeded(); - void onDownloadFailed(QString reason); + void onManifestDownloadSucceeded(); + void onResolveModsSucceeded(); + void onModDownloadSucceeded(); + + void onManifestDownloadFailed(QString reason); + void onResolveModsFailed(QString reason); + void onModDownloadFailed(QString reason); private: + void resolveMods(); void downloadPack(); void install(); private: - bool abortable = false; + bool m_abortable = true; + + NetJob::Ptr m_net_job = nullptr; + shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver_task = nullptr; + + QList<int> m_file_id_map; - NetJob::Ptr jobPtr; - QByteArray response; + QByteArray m_response; Modpack m_pack; QString m_version_name; Version m_version; - QMap<QString, QString> filesToCopy; + QMap<QString, QString> m_files_to_copy; + //FIXME: nuke + QWidget* m_parent; }; } diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp index e2d47a5b..421527ae 100644 --- a/launcher/modplatform/modpacksch/FTBPackManifest.cpp +++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FTBPackManifest.h" @@ -127,13 +146,16 @@ static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj) a.path = Json::requireString(obj, "path"); a.name = Json::requireString(obj, "name"); a.version = Json::requireString(obj, "version"); - a.url = Json::requireString(obj, "url"); + a.url = Json::ensureString(obj, "url"); // optional a.sha1 = Json::requireString(obj, "sha1"); a.size = Json::requireInteger(obj, "size"); a.clientOnly = Json::requireBoolean(obj, "clientonly"); a.serverOnly = Json::requireBoolean(obj, "serveronly"); a.optional = Json::requireBoolean(obj, "optional"); a.updated = Json::requireInteger(obj, "updated"); + auto curseforgeObj = Json::ensureObject(obj, "curseforge"); // optional + a.curseforge.project_id = Json::ensureInteger(curseforgeObj, "project"); + a.curseforge.file_id = Json::ensureInteger(curseforgeObj, "file"); } void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj) diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h index da45d8ac..a8b6f35e 100644 --- a/launcher/modplatform/modpacksch/FTBPackManifest.h +++ b/launcher/modplatform/modpacksch/FTBPackManifest.h @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -97,6 +116,12 @@ struct VersionTarget int64_t updated; }; +struct VersionFileCurseForge +{ + int project_id; + int file_id; +}; + struct VersionFile { int id; @@ -111,6 +136,7 @@ struct VersionFile bool serverOnly; bool optional; int64_t updated; + VersionFileCurseForge curseforge; }; struct Version diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp new file mode 100644 index 00000000..747cf4c3 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -0,0 +1,108 @@ +#include "ModrinthAPI.h" + +#include "Application.h" +#include "Json.h" +#include "net/Upload.h" + +auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); + + netJob->addNetAction(Net::Download::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::latestVersion(QString hash, + QString hash_format, + std::list<Version> mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + + QStringList game_versions; + for (auto& ver : mcVersions) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::latestVersions(const QStringList& hashes, + QString hash_format, + std::list<Version> mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + + QStringList game_versions; + for (auto& ver : mcVersions) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* +{ + auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); + auto searchUrl = getMultipleModInfoURL(addonIds); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); + + return netJob; +} diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 89e52d6c..e1a18681 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -27,6 +27,29 @@ class ModrinthAPI : public NetworkModAPI { public: + auto currentVersion(QString hash, + QString hash_format, + QByteArray* response) -> NetJob::Ptr; + + auto currentVersions(const QStringList& hashes, + QString hash_format, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersion(QString hash, + QString hash_format, + std::list<Version> mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersions(const QStringList& hashes, + QString hash_format, + std::list<Version> mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + + auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; + + public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList @@ -81,6 +104,11 @@ class ModrinthAPI : public NetworkModAPI { return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; }; + inline auto getMultipleModInfoURL(QStringList ids) const -> QString + { + return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { return QString(BuildConfig.MODRINTH_PROD_URL + diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp new file mode 100644 index 00000000..79d8edf7 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -0,0 +1,174 @@ +#include "ModrinthCheckUpdate.h" +#include "ModrinthAPI.h" +#include "ModrinthPackIndex.h" + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static ModrinthAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; + +bool ModrinthCheckUpdate::abort() +{ + if (m_net_job) + return m_net_job->abort(); + return true; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void ModrinthCheckUpdate::executeTask() +{ + setStatus(tr("Preparing mods for Modrinth...")); + setProgress(0, 3); + + QHash<QString, Mod*> mappings; + + // Create all hashes + QStringList hashes; + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + for (auto* mod : m_mods) { + if (!mod->enabled()) { + emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); + continue; + } + + auto hash = mod->metadata()->hash; + + // Sadly the API can only handle one hash type per call, se we + // need to generate a new hash if the current one is innadequate + // (though it will rarely happen, if at all) + if (mod->metadata()->hash_format != best_hash_type) { + QByteArray jar_data; + + try { + jar_data = FS::read(mod->fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name()); + qCritical() << QString("Reason: ") << e.cause(); + + failed(e.what()); + return; + } + + hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex()); + } + + hashes.append(hash); + mappings.insert(hash, mod); + } + + auto* response = new QByteArray(); + auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); + + QEventLoop lock; + + connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + setStatus(tr("Parsing the API response from Modrinth...")); + setProgress(2, 3); + + try { + for (auto hash : mappings.keys()) { + auto project_obj = doc[hash].toObject(); + + // If the returned project is empty, but we have Modrinth metadata, + // it means this specific version is not available + if (project_obj.isEmpty()) { + qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response."; + qDebug() << "Hash: " << hash; + + emit checkFailed( + mappings.find(hash).value(), + tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); + + continue; + } + + // Sometimes a version may have multiple files, one with "forge" and one with "fabric", + // so we may want to filter it + QString loader_filter; + static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt }; + for (auto flag : flags) { + if (m_loaders.testFlag(flag)) { + loader_filter = api.getModLoaderString(flag); + break; + } + } + + // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: + // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter + // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) + // Such is the pain of having arbitrary files for a given version .-. + + auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter); + if (project_ver.downloadUrl.isEmpty()) { + qCritical() << "Modrinth mod without download url!"; + qCritical() << project_ver.fileName; + + emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL")); + + continue; + } + + auto mod_iter = mappings.find(hash); + if (mod_iter == mappings.end()) { + qCritical() << "Failed to remap mod from Modrinth!"; + continue; + } + auto mod = *mod_iter; + + auto key = project_ver.hash; + if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) { + if (mod->version() == project_ver.version_number) + continue; + + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod->name(); + pack.slug = mod->metadata()->slug; + pack.addonId = mod->metadata()->project_id; + pack.websiteUrl = mod->homeurl(); + for (auto& author : mod->authors()) + pack.authors.append({ author }); + pack.description = mod->description(); + pack.provider = ModPlatform::Provider::MODRINTH; + + auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder); + + m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog, + ModPlatform::Provider::MODRINTH, download_task); + } + } + } catch (Json::JsonException& e) { + failed(e.cause() + " : " + e.what()); + } + }); + + connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); + + setStatus(tr("Waiting for the API response from Modrinth...")); + setProgress(1, 3); + + m_net_job = job.get(); + job->start(); + + lock.exec(); + + emitSucceeded(); +} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h new file mode 100644 index 00000000..abf8ada1 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Application.h" +#include "modplatform/CheckUpdateTask.h" +#include "net/NetJob.h" + +class ModrinthCheckUpdate : public CheckUpdateTask { + Q_OBJECT + + public: + ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder) + : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + + private: + NetJob* m_net_job = nullptr; +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index b6f5490a..e50dd96d 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -29,13 +29,16 @@ static ModPlatform::ProviderCapabilities ProviderCaps; void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - pack.addonId = Json::requireString(obj, "project_id"); + pack.addonId = Json::ensureString(obj, "project_id"); + if (pack.addonId.toString().isEmpty()) + pack.addonId = Json::requireString(obj, "id"); + pack.provider = ModPlatform::Provider::MODRINTH; pack.name = Json::requireString(obj, "title"); - QString slug = Json::ensureString(obj, "slug", ""); - if (!slug.isEmpty()) - pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", ""); + pack.slug = Json::ensureString(obj, "slug", ""); + if (!pack.slug.isEmpty()) + pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; else pack.websiteUrl = ""; @@ -45,7 +48,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.logoName = pack.addonId.toString(); ModPlatform::ModpackAuthor modAuthor; - modAuthor.name = Json::requireString(obj, "author"); + modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)")); modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); @@ -111,7 +114,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versionsLoaded = true; } -auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion +auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion { ModPlatform::IndexedVersion file; @@ -130,6 +133,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); + file.version_number = Json::requireString(obj, "version_number"); + file.changelog = Json::requireString(obj, "changelog"); auto files = Json::requireArray(obj, "files"); int i = 0; @@ -142,6 +147,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); + if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) { + file.is_preferred = true; + break; + } + // Grab the primary file, if available if (Json::requireBoolean(parent, "primary")) break; @@ -153,13 +163,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); + file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); 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; + + if (hash_list.contains(preferred_hash_type)) { + file.hash = Json::requireString(hash_list, preferred_hash_type); + file.hash_type = preferred_hash_type; + } else { + 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; + } } } diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index b7936204..31881414 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 0782b9f4..c3561093 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -55,11 +55,11 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin } // Helpers -static inline auto indexFileName(QString const& mod_name) -> QString +static inline auto indexFileName(QString const& mod_slug) -> QString { - if(mod_name.endsWith(".pw.toml")) - return mod_name; - return QString("%1.pw.toml").arg(mod_name); + if(mod_slug.endsWith(".pw.toml")) + return mod_slug; + return QString("%1.pw.toml").arg(mod_slug); } static ModPlatform::ProviderCapabilities ProviderCaps; @@ -95,6 +95,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo { Mod mod; + mod.slug = mod_pack.slug; mod.name = mod_pack.name; mod.filename = mod_version.fileName; @@ -116,12 +117,10 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo return mod; } -auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod +auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod { - auto mod_name = internal_mod.name(); - // Try getting metadata if it exists - Mod mod { getIndexForMod(index_dir, mod_name) }; + Mod mod { getIndexForMod(index_dir, slug) }; if(mod.isValid()) return mod; @@ -139,11 +138,14 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) // Ensure the corresponding mod's info exists, and create it if not - auto normalized_fname = indexFileName(mod.name); + auto normalized_fname = indexFileName(mod.slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); QFile index_file(index_dir.absoluteFilePath(real_fname)); + if (real_fname != normalized_fname) + index_file.rename(normalized_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 @@ -184,33 +186,46 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) } } + index_file.flush(); index_file.close(); } -void V1::deleteModIndex(QDir& index_dir, QString& mod_name) +void V1::deleteModIndex(QDir& index_dir, QString& mod_slug) { - auto normalized_fname = indexFileName(mod_name); + auto normalized_fname = indexFileName(mod_slug); 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); + if (!index_file.exists()) { + qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug); return; } - if(!index_file.remove()){ - qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); + if (!index_file.remove()) { + qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug); + } +} + +void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) +{ + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) { + deleteModIndex(index_dir, mod.name); + break; + } } } -auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod +auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod { Mod mod; - auto normalized_fname = indexFileName(index_file_name); + auto normalized_fname = indexFileName(slug); auto real_fname = getRealIndexName(index_dir, normalized_fname, true); if (real_fname.isEmpty()) return {}; @@ -218,7 +233,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod 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); + qWarning() << QString("Failed to open mod metadata for %1").arg(slug); return {}; } @@ -232,11 +247,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod index_file.close(); if (!table) { - qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name)); + qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason: " << QString(errbuf); return {}; } + mod.slug = slug; + { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); @@ -286,4 +303,16 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod return mod; } -} // namespace Packwiz +auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod +{ + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) + return mod; + } + + return {}; +} + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 3c99769c..3ec80377 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -40,6 +40,7 @@ auto intEntry(toml_table_t* parent, const char* entry_name) -> int; class V1 { public: struct Mod { + QString slug {}; QString name {}; QString filename {}; // FIXME: make side an enum @@ -58,7 +59,7 @@ class V1 { public: // This is a totally heuristic, but should work for now. - auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); } + auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } // Different providers can use different names for the same thing // Modrinth-specific @@ -71,9 +72,9 @@ class V1 { * */ 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. + * its common representation in the launcher, plus a necessary slug. * */ - static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod; + static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already @@ -81,13 +82,21 @@ class V1 { * */ 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); + /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QString& mod_slug); - /* Gets the metadata for a mod with a particular name. + /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QVariant& mod_id); + + /* Gets the metadata for a mod with a particular file name. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod; + + /* Gets the metadata for a mod with a particular id. * 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; + static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; }; } // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index d6251148..aa0c35df 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -32,10 +32,11 @@ class PackwizTest : public QObject { QString source = QFINDTESTDATA("testdata"); QDir index_dir(source); - QString name_mod("borderless-mining.pw.toml"); - QVERIFY(index_dir.entryList().contains(name_mod)); + QString slug_mod("borderless-mining"); + QString file_name = slug_mod + ".pw.toml"; + QVERIFY(index_dir.entryList().contains(file_name)); - auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod); QVERIFY(metadata.isValid()); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index 4d1fcbff..981ccf8a 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -24,7 +24,7 @@ #include <QStringList> #include <QUrl> -#include <nonstd/optional> +#include <optional> namespace Technic { @@ -57,8 +57,8 @@ private: QString m_archivePath; NetJob::Ptr m_filesNetJob; std::unique_ptr<QuaZip> m_packZip; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; }; } // namespace Technic diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index d93eb088..e6a6adcc 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.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 @@ -117,8 +118,9 @@ void Download::executeTask() } request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); - if (request.url().host().contains("api.curseforge.com")) { - request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); + if (APPLICATION->currentCapabilities() & Application::SupportsFlame + && request.url().host().contains("api.curseforge.com")) { + request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); }; QNetworkReply* rep = m_network->get(request); @@ -126,7 +128,11 @@ void Download::executeTask() m_reply.reset(rep); 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); } diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 20932944..1d264381 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -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 diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 4d86c0b8..deb2780b 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -121,6 +121,14 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex SaveEventually(); } + // Get rid of old entries, to prevent cache problems + auto current_time = QDateTime::currentSecsSinceEpoch(); + if (entry->isExpired(current_time - ( file_last_changed / 1000 ))) { + qWarning() << "Removing cache entry because of old age!"; + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + // entry passed all the checks we cared about. entry->basePath = getBasePath(base); return entry; @@ -221,6 +229,8 @@ void HttpMetaCache::Load() foo->etag = Json::ensureString(element_obj, "etag"); foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); + foo->current_age = Json::ensureDouble(element_obj, "current_age"); + foo->max_age = Json::ensureDouble(element_obj, "max_age"); // presumed innocent until closer examination foo->stale = false; @@ -240,6 +250,8 @@ void HttpMetaCache::SaveNow() if (m_index_file.isNull()) return; + qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries"; + QJsonObject toplevel; Json::writeString(toplevel, "version", "1"); @@ -259,6 +271,8 @@ void HttpMetaCache::SaveNow() entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); if (!entry->remote_changed_timestamp.isEmpty()) entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); + entryObj.insert("current_age", QJsonValue(double(entry->current_age))); + entryObj.insert("max_age", QJsonValue(double(entry->max_age))); entriesArr.append(entryObj); } } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index e944b3d5..df3549e8 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -64,6 +64,14 @@ class MetaEntry { auto getMD5Sum() -> QString { return md5sum; } void setMD5Sum(QString md5sum) { this->md5sum = md5sum; } + auto getCurrentAge() -> qint64 { return current_age; } + void setCurrentAge(qint64 age) { current_age = age; } + + auto getMaximumAge() -> qint64 { return max_age; } + void setMaximumAge(qint64 age) { max_age = age; } + + bool isExpired(qint64 offset) { return current_age >= max_age - offset; }; + protected: QString baseId; QString basePath; @@ -72,6 +80,8 @@ class MetaEntry { QString etag; qint64 local_changed_timestamp = 0; QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time + qint64 current_age = 0; + qint64 max_age = 0; bool stale = true; }; diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index f86dd870..ab0c9fcb 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -36,11 +36,16 @@ #include "MetaCacheSink.h" #include <QFile> #include <QFileInfo> -#include "FileSystem.h" #include "Application.h" namespace Net { +/** Maximum time to hold a cache entry + * = 1 week in seconds + */ +#define MAX_TIME_TO_EXPIRE 1*7*24*60*60 + + MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum) :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum) { @@ -88,6 +93,37 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply) } m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); + + { // Cache lifetime + if (reply.hasRawHeader("Cache-Control")) { + auto cache_control_header = reply.rawHeader("Cache-Control"); + // qDebug() << "[MetaCache] Parsing 'Cache-Control' header with" << cache_control_header; + + QRegularExpression max_age_expr("max-age=([0-9]+)"); + qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong(); + m_entry->setMaximumAge(max_age); + + } else if (reply.hasRawHeader("Expires")) { + auto expires_header = reply.rawHeader("Expires"); + // qDebug() << "[MetaCache] Parsing 'Expires' header with" << expires_header; + + qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch(); + m_entry->setMaximumAge(max_age); + } else { + m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE); + } + + if (reply.hasRawHeader("Age")) { + auto age_header = reply.rawHeader("Age"); + // qDebug() << "[MetaCache] Parsing 'Age' header with" << age_header; + + qint64 current_age = age_header.toLongLong(); + m_entry->setCurrentAge(current_age); + } else { + m_entry->setCurrentAge(0); + } + } + m_entry->setStale(false); APPLICATION->metacache()->updateEntry(m_entry); diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index df899178..bab35fa5 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.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 @@ -97,11 +98,16 @@ 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.toList(); + auto toKill = m_doing.values(); for (auto index : toKill) { auto part = m_downloads[index]; fullyAborted &= part->abort(); diff --git a/launcher/net/NetUtils.h b/launcher/net/NetUtils.h new file mode 100644 index 00000000..fa3bd8c0 --- /dev/null +++ b/launcher/net/NetUtils.h @@ -0,0 +1,44 @@ +// 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/>. + */ + +#pragma once + +#include <QNetworkReply> +#include <QSet> + +namespace Net { + inline bool isApplicationError(QNetworkReply::NetworkError x) { + // Mainly taken from https://github.com/qt/qtbase/blob/dev/src/network/access/qhttpthreaddelegate.cpp + static QSet<QNetworkReply::NetworkError> errors = { + QNetworkReply::ProtocolInvalidOperationError, + QNetworkReply::AuthenticationRequiredError, + QNetworkReply::ContentAccessDenied, + QNetworkReply::ContentNotFoundError, + QNetworkReply::ContentOperationNotPermittedError, + QNetworkReply::ProxyAuthenticationRequiredError, + QNetworkReply::ContentConflictError, + QNetworkReply::ContentGoneError, + QNetworkReply::InternalServerError, + QNetworkReply::OperationNotImplementedError, + QNetworkReply::ServiceUnavailableError, + QNetworkReply::UnknownServerError, + QNetworkReply::UnknownContentError + }; + return errors.contains(x); + } +} // namespace Net diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 835e4cd1..76b86743 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -129,10 +129,13 @@ void PasteUpload::executeTask() connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); - // This function call would be a lot shorter if we were using the latest Qt - connect(rep, - static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), - this, &PasteUpload::downloadError); + +#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); diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index c9942a8d..cfda4b4e 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -1,6 +1,38 @@ -// -// Created by timoreo on 20/05/22. -// +// 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 "Upload.h" @@ -11,6 +43,16 @@ namespace Net { + bool Upload::abort() + { + if (m_reply) { + m_reply->abort(); + } else { + m_state = State::AbortedByUser; + } + return true; + } + void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); } @@ -174,8 +216,9 @@ namespace Net { } request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); - if (request.url().host().contains("api.curseforge.com")) { - request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); + if (APPLICATION->currentCapabilities() & Application::SupportsFlame + && request.url().host().contains("api.curseforge.com")) { + request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); } //TODO other types of post requests ? request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index ee784c6e..7c194bbc 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "NetAction.h" @@ -10,6 +46,8 @@ namespace Net { public: static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; 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/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 04e26ea2..a72c32d3 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.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 @@ -73,7 +74,11 @@ void ImgurAlbumCreation::executeTask() 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) { diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 9aeb6fb8..f8ac9bc2 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.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 @@ -88,8 +89,11 @@ void ImgurUpload::executeTask() 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) { 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/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp new file mode 100644 index 00000000..6e853568 --- /dev/null +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -0,0 +1,48 @@ +#include "MultipleOptionsTask.h" + +#include <QDebug> + +MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : SequentialTask(parent, task_name) {} + +void MultipleOptionsTask::startNext() +{ + Task* previous = nullptr; + if (m_currentIndex != -1) { + previous = m_queue[m_currentIndex].get(); + disconnect(previous, 0, this, 0); + } + + m_currentIndex++; + if ((previous && previous->wasSuccessful())) { + emitSucceeded(); + return; + } + + Task::Ptr next = m_queue[m_currentIndex]; + + connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed); + connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext); + + connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus); + connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus); + + connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress); + + qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()); + setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + + next->start(); +} + +void MultipleOptionsTask::subTaskFailed(QString const& reason) +{ + qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason); + if(m_currentIndex < m_queue.size() - 1) { + startNext(); + return; + } + + qWarning() << QString("All attempts have failed!"); + emitFailed(); +} diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h new file mode 100644 index 00000000..7c508b00 --- /dev/null +++ b/launcher/tasks/MultipleOptionsTask.h @@ -0,0 +1,19 @@ +#pragma once + +#include "SequentialTask.h" + +/* This task type will attempt to do run each of it's subtasks in sequence, + * until one of them succeeds. When that happens, the remaining tasks will not run. + * */ +class MultipleOptionsTask : public SequentialTask +{ + Q_OBJECT +public: + explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = ""); + virtual ~MultipleOptionsTask() = default; + +private +slots: + void startNext() override; + void subTaskFailed(const QString &msg) override; +}; diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 7f03ad2e..f1e1a889 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -1,5 +1,7 @@ #include "SequentialTask.h" +#include <QDebug> + SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {} SequentialTask::~SequentialTask() @@ -39,14 +41,15 @@ bool SequentialTask::abort() emit aborted(); emit finished(); } - m_queue.clear(); + + m_aborted = true; return true; } bool succeeded = m_queue[m_currentIndex]->abort(); - m_queue.clear(); + m_aborted = succeeded; - if(succeeded) + if (succeeded) emitAborted(); return succeeded; @@ -54,10 +57,14 @@ bool SequentialTask::abort() void SequentialTask::startNext() { - if (m_currentIndex != -1) { - Task::Ptr previous = m_queue[m_currentIndex]; + if (m_aborted) + return; + + if (m_currentIndex != -1 && m_currentIndex < m_queue.size()) { + Task::Ptr previous = m_queue.at(m_currentIndex); disconnect(previous.get(), 0, this, 0); } + m_currentIndex++; if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) { emitSucceeded(); @@ -76,6 +83,8 @@ void SequentialTask::startNext() setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + setProgress(m_currentIndex + 1, m_queue.count()); + next->start(); } @@ -93,7 +102,6 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total) setProgress(0, 100); return; } - 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 e10cb6f7..f5a58b1b 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -20,17 +20,17 @@ public: void addTask(Task::Ptr task); -protected slots: - void executeTask() override; public slots: bool abort() override; -private +protected slots: - void startNext(); - void subTaskFailed(const QString &msg); - void subTaskStatus(const QString &msg); - void subTaskProgress(qint64 current, qint64 total); + void executeTask() override; + + virtual void startNext(); + virtual void subTaskFailed(const QString &msg); + virtual void subTaskStatus(const QString &msg); + virtual void subTaskProgress(qint64 current, qint64 total); protected: void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; @@ -44,4 +44,6 @@ protected: qint64 m_stepProgress = 0; qint64 m_stepTotalProgress = 100; + + bool m_aborted = false; }; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 53722d69..848b4d19 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.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 @@ -52,7 +53,7 @@ #include "Application.h" -const static QLatin1Literal defaultLangCode("en_US"); +const static QLatin1String defaultLangCode("en_US"); enum class FileType { @@ -431,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); } } } 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 aeff8073..c3d95599 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -42,31 +42,31 @@ #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> @@ -235,7 +235,6 @@ public: TranslatedAction actionMods; TranslatedAction actionViewSelectedInstFolder; TranslatedAction actionViewSelectedMCFolder; - TranslatedAction actionViewSelectedModsFolder; TranslatedAction actionDeleteInstance; TranslatedAction actionConfig_Folder; TranslatedAction actionCAT; @@ -253,6 +252,9 @@ public: TranslatedAction actionViewInstanceFolder; TranslatedAction actionViewCentralModsFolder; + QMenu * editMenu = nullptr; + TranslatedAction actionUndoTrashInstance; + QMenu * helpMenu = nullptr; TranslatedToolButton helpMenuButton; TranslatedAction actionReportBug; @@ -285,7 +287,7 @@ public: TranslatedToolbar newsToolBar; QVector<TranslatedToolbar *> all_toolbars; - void createMainToolbarActions(QMainWindow *MainWindow) + void createMainToolbarActions(MainWindow *MainWindow) { actionAddInstance = TranslatedAction(MainWindow); actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); @@ -336,6 +338,14 @@ public: actionSettings->setShortcut(QKeySequence::Preferences); all_actions.append(&actionSettings); + actionUndoTrashInstance = TranslatedAction(MainWindow); + connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), MainWindow, SLOT(undoTrashInstance())); + actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance")); + actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion")); + actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + actionUndoTrashInstance->setShortcut(QKeySequence("Ctrl+Z")); + all_actions.append(&actionUndoTrashInstance); + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { actionReportBug = TranslatedAction(MainWindow); actionReportBug->setObjectName(QStringLiteral("actionReportBug")); @@ -509,6 +519,9 @@ public: fileMenu->addSeparator(); fileMenu->addAction(actionSettings); + editMenu = menuBar->addMenu(tr("&Edit")); + editMenu->addAction(actionUndoTrashInstance); + viewMenu = menuBar->addMenu(tr("&View")); viewMenu->setSeparatorsCollapsible(false); viewMenu->addAction(actionCAT); @@ -709,14 +722,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")); @@ -741,9 +746,10 @@ public: actionDeleteInstance = TranslatedAction(MainWindow); actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance...")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance")); actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); + actionDeleteInstance->setAutoRepeat(false); all_actions.append(&actionDeleteInstance); actionCopyInstance = TranslatedAction(MainWindow); @@ -793,9 +799,6 @@ public: instanceToolBar->addSeparator(); instanceToolBar->addAction(actionViewSelectedMCFolder); - /* - instanceToolBar->addAction(actionViewSelectedModsFolder); - */ instanceToolBar->addAction(actionConfig_Folder); instanceToolBar->addAction(actionViewSelectedInstFolder); @@ -1041,6 +1044,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow { updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); } + + if (APPLICATION->updateChecker()->getExternalUpdater()) + { + connect(APPLICATION->updateChecker()->getExternalUpdater(), + &ExternalUpdater::canCheckForUpdatesChanged, + this, + &MainWindow::updatesAllowedChanged); + } } setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); @@ -1154,6 +1165,11 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup())); actions.append(actionDeleteGroup); } + + QAction *actionUndoTrashInstance = new QAction("Undo last trash instance", this); + connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), SLOT(undoTrashInstance())); + actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + actions.append(actionUndoTrashInstance); } QMenu myMenu; myMenu.addActions(actions); @@ -1494,7 +1510,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) { @@ -1832,6 +1852,11 @@ void MainWindow::deleteGroup() } } +void MainWindow::undoTrashInstance() +{ + APPLICATION->instances()->undoTrashInstance(); +} + void MainWindow::on_actionViewInstanceFolder_triggered() { QString str = APPLICATION->settings()->get("InstanceDir").toString(); @@ -1890,11 +1915,6 @@ void MainWindow::globalSettingsClosed() update(); } -void MainWindow::on_actionInstanceSettings_triggered() -{ - APPLICATION->showInstanceWindow(m_selectedInstance, "settings"); -} - void MainWindow::on_actionEditInstNotes_triggered() { APPLICATION->showInstanceWindow(m_selectedInstance, "notes"); @@ -1962,7 +1982,12 @@ void MainWindow::on_actionDeleteInstance_triggered() { return; } + auto id = m_selectedInstance->id(); + if (APPLICATION->instances()->trashInstance(id)) { + return; + } + auto response = CustomMessageBox::selectable( this, tr("CAREFUL!"), @@ -2017,20 +2042,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. diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 0ca8ec7b..dde3d02c 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -118,8 +118,6 @@ private slots: void on_actionViewSelectedMCFolder_triggered(); - void on_actionViewSelectedModsFolder_triggered(); - void refreshInstances(); void on_actionViewCentralModsFolder_triggered(); @@ -128,8 +126,6 @@ private slots: void on_actionSettings_triggered(); - void on_actionInstanceSettings_triggered(); - void on_actionManageAccounts_triggered(); void on_actionReportBug_triggered(); @@ -149,6 +145,7 @@ private slots: void on_actionDeleteInstance_triggered(); void deleteGroup(); + void undoTrashInstance(); void on_actionExportInstance_triggered(); diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 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/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp new file mode 100644 index 00000000..89935d9a --- /dev/null +++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -0,0 +1,96 @@ +#include "ChooseProviderDialog.h" +#include "ui_ChooseProviderDialog.h" + +#include <QPushButton> +#include <QRadioButton> + +#include "modplatform/ModIndex.h" + +static ModPlatform::ProviderCapabilities ProviderCaps; + +ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping) + : QDialog(parent), ui(new Ui::ChooseProviderDialog) +{ + ui->setupUi(this); + + addProviders(); + m_providers.button(0)->click(); + + connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne); + connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll); + + connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne); + connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll); + + if (single_choice) { + ui->providersLayout->removeWidget(ui->skipAllButton); + ui->providersLayout->removeWidget(ui->confirmAllButton); + } + + if (!allow_skipping) { + ui->providersLayout->removeWidget(ui->skipOneButton); + ui->providersLayout->removeWidget(ui->skipAllButton); + } +} + +ChooseProviderDialog::~ChooseProviderDialog() +{ + delete ui; +} + +void ChooseProviderDialog::setDescription(QString desc) +{ + ui->explanationLabel->setText(desc); +} + +void ChooseProviderDialog::skipOne() +{ + reject(); +} +void ChooseProviderDialog::skipAll() +{ + m_response.skip_all = true; + reject(); +} + +void ChooseProviderDialog::confirmOne() +{ + m_response.chosen = getSelectedProvider(); + m_response.try_others = ui->tryOthersCheckbox->isChecked(); + accept(); +} +void ChooseProviderDialog::confirmAll() +{ + m_response.chosen = getSelectedProvider(); + m_response.confirm_all = true; + m_response.try_others = ui->tryOthersCheckbox->isChecked(); + accept(); +} + +auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider +{ + return ModPlatform::Provider(m_providers.checkedId()); +} + +void ChooseProviderDialog::addProviders() +{ + int btn_index = 0; + QRadioButton* btn; + + for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) { + btn = new QRadioButton(ProviderCaps.readableName(provider), this); + m_providers.addButton(btn, btn_index++); + ui->providersLayout->addWidget(btn); + } +} + +void ChooseProviderDialog::disableInput() +{ + for (auto& btn : m_providers.buttons()) + btn->setEnabled(false); + + ui->skipOneButton->setEnabled(false); + ui->skipAllButton->setEnabled(false); + ui->confirmOneButton->setEnabled(false); + ui->confirmAllButton->setEnabled(false); +} diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h new file mode 100644 index 00000000..4a3b9f29 --- /dev/null +++ b/launcher/ui/dialogs/ChooseProviderDialog.h @@ -0,0 +1,56 @@ +#pragma once + +#include <QButtonGroup> +#include <QDialog> + +namespace Ui { +class ChooseProviderDialog; +} + +namespace ModPlatform { +enum class Provider; +} + +class Mod; +class NetJob; +class ModUpdateDialog; + +class ChooseProviderDialog : public QDialog { + Q_OBJECT + + struct Response { + bool skip_all = false; + bool confirm_all = false; + + bool try_others = false; + + ModPlatform::Provider chosen; + }; + + public: + explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true); + ~ChooseProviderDialog(); + + auto getResponse() const -> Response { return m_response; } + + void setDescription(QString desc); + + private slots: + void skipOne(); + void skipAll(); + void confirmOne(); + void confirmAll(); + + private: + void addProviders(); + void disableInput(); + + auto getSelectedProvider() const -> ModPlatform::Provider; + + private: + Ui::ChooseProviderDialog* ui; + + QButtonGroup m_providers; + + Response m_response; +}; diff --git a/launcher/ui/dialogs/ChooseProviderDialog.ui b/launcher/ui/dialogs/ChooseProviderDialog.ui new file mode 100644 index 00000000..78cd9613 --- /dev/null +++ b/launcher/ui/dialogs/ChooseProviderDialog.ui @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ChooseProviderDialog</class> + <widget class="QDialog" name="ChooseProviderDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>453</width> + <height>197</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose a mod provider</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="explanationLabel"> + <property name="alignment"> + <set>Qt::AlignJustify|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="indent"> + <number>-1</number> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <layout class="QFormLayout" name="providersLayout"> + <property name="labelAlignment"> + <set>Qt::AlignHCenter|Qt::AlignTop</set> + </property> + <property name="formAlignment"> + <set>Qt::AlignHCenter|Qt::AlignTop</set> + </property> + </layout> + </item> + <item row="4" column="0" colspan="2"> + <layout class="QHBoxLayout" name="buttonsLayout"> + <item> + <widget class="QPushButton" name="skipOneButton"> + <property name="text"> + <string>Skip this mod</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="skipAllButton"> + <property name="text"> + <string>Skip all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="confirmAllButton"> + <property name="text"> + <string>Confirm for all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="confirmOneButton"> + <property name="text"> + <string>Confirm</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="tryOthersCheckbox"> + <property name="text"> + <string>Try to automatically use other providers if the chosen one fails</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> 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 f01c9c07..e4fc3ecc 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -1,9 +1,28 @@ +// 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/>. + */ + #include "ModDownloadDialog.h" #include <BaseVersion.h> #include <icons/IconList.h> #include <InstanceList.h> +#include "Application.h" #include "ProgressDialog.h" #include "ReviewMessageBox.h" @@ -100,13 +119,13 @@ void ModDownloadDialog::accept() QList<BasePage *> ModDownloadDialog::getPages() { - modrinthPage = new ModrinthModPage(this, m_instance); - flameModPage = new FlameModPage(this, m_instance); - return - { - modrinthPage, - flameModPage - }; + QList<BasePage *> pages; + + pages.append(new ModrinthModPage(this, m_instance)); + if (APPLICATION->currentCapabilities() & Application::SupportsFlame) + pages.append(new FlameModPage(this, m_instance)); + + return pages; } void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task) diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 5c565ad3..1fa1f058 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -1,3 +1,21 @@ +// 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/>. + */ + #pragma once #include <QDialog> @@ -48,9 +66,6 @@ private: QDialogButtonBox * m_buttons = nullptr; QVBoxLayout *m_verticalLayout = nullptr; - - ModrinthModPage *modrinthPage = nullptr; - FlameModPage *flameModPage = nullptr; QHash<QString, ModDownloadTask*> modTask; BaseInstance *m_instance; }; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp new file mode 100644 index 00000000..d73c8ebb --- /dev/null +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -0,0 +1,409 @@ +#include "ModUpdateDialog.h" +#include "ChooseProviderDialog.h" +#include "CustomMessageBox.h" +#include "ProgressDialog.h" +#include "ScrollMessageBox.h" +#include "ui_ReviewMessageBox.h" + +#include "FileSystem.h" +#include "Json.h" + +#include "tasks/ConcurrentTask.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/EnsureMetadataTask.h" +#include "modplatform/flame/FlameCheckUpdate.h" +#include "modplatform/modrinth/ModrinthCheckUpdate.h" + +#include <HoeDown.h> +#include <QTextBrowser> +#include <QTreeWidgetItem> + +static ModPlatform::ProviderCapabilities ProviderCaps; + +static std::list<Version> mcVersions(BaseInstance* inst) +{ + return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; +} + +static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() }; +} + +ModUpdateDialog::ModUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr<ModFolderModel> mods, + QList<Mod::Ptr>& search_for) + : ReviewMessageBox(parent, tr("Confirm mods to update"), "") + , m_parent(parent) + , m_mod_model(mods) + , m_candidates(search_for) + , m_second_try_metadata(new ConcurrentTask()) + , m_instance(instance) +{ + ReviewMessageBox::setGeometry(0, 0, 800, 600); + + ui->explainLabel->setText(tr("You're about to update the following mods:")); + ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); +} + +void ModUpdateDialog::checkCandidates() +{ + // Ensure mods have valid metadata + auto went_well = ensureMetadata(); + if (!went_well) { + m_aborted = true; + return; + } + + // Report failed metadata generation + if (!m_failed_metadata.empty()) { + QString text; + for (const auto& failed : m_failed_metadata) { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + text += tr("Mod name: %1<br>File name: %2<br>Reason: %3<br><br>").arg(mod->name(), mod->fileinfo().fileName(), reason); + } + + ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), + tr("Could not generate metadata for the following mods:<br>" + "Do you wish to proceed without those mods?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + auto versions = mcVersions(m_instance); + auto loaders = mcLoaders(m_instance); + + SequentialTask check_task(m_parent, tr("Checking for updates")); + + if (!m_modrinth_to_update.empty()) { + m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); + connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, + [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); }); + check_task.addTask(m_modrinth_check_task); + } + + if (!m_flame_to_update.empty()) { + m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); + connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, + [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); }); + check_task.addTask(m_flame_check_task); + } + + connect(&check_task, &Task::failed, this, + [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect(&check_task, &Task::succeeded, this, [&]() { + QStringList warnings = check_task.warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + + // Check for updates + ProgressDialog progress_dialog(m_parent); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setWindowTitle(tr("Checking for updates...")); + auto ret = progress_dialog.execWithTask(&check_task); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + + // Add found updates for Modrinth + if (m_modrinth_check_task) { + auto modrinth_updates = m_modrinth_check_task->getUpdatable(); + for (auto& updatable : modrinth_updates) { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendMod(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + } + + // Add found updated for Flame + if (m_flame_check_task) { + auto flame_updates = m_flame_check_task->getUpdatable(); + for (auto& updatable : flame_updates) { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendMod(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + } + + // Report failed update checking + if (!m_failed_check_update.empty()) { + QString text; + for (const auto& failed : m_failed_check_update) { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + const auto& recover_url = std::get<2>(failed); + + qDebug() << mod->name() << " failed to check for updates!"; + + text += tr("Mod name: %1").arg(mod->name()) + "<br>"; + if (!reason.isEmpty()) + text += tr("Reason: %1").arg(reason) + "<br>"; + if (!recover_url.isEmpty()) + //: %1 is the link to download it manually + text += tr("Possible solution: Getting the latest version manually:<br>%1<br>") + .arg(QString("<a href='%1'>%1</a>").arg(recover_url.toString())); + text += "<br>"; + } + + ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), + tr("Could not check or get the following mods for updates:<br>" + "Do you wish to proceed without those mods?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + // If there's no mod to be updated + if (ui->modTreeWidget->topLevelItemCount() == 0) { + m_no_updates = true; + } else { + // FIXME: Find a more efficient way of doing this! + + // Sort major items in alphabetical order (also sorts the children unfortunately) + ui->modTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder); + + // Re-sort the children + auto* item = ui->modTreeWidget->topLevelItem(0); + for (int i = 1; item != nullptr; ++i) { + item->sortChildren(0, Qt::SortOrder::DescendingOrder); + item = ui->modTreeWidget->topLevelItem(i); + } + } + + if (m_aborted || m_no_updates) + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); +} + +// Part 1: Ensure we have a valid metadata +auto ModUpdateDialog::ensureMetadata() -> bool +{ + auto index_dir = indexDir(); + + SequentialTask seq(m_parent, tr("Looking for metadata")); + + // A better use of data structures here could remove the need for this QHash + QHash<QString, bool> should_try_others; + QList<Mod*> modrinth_tmp; + QList<Mod*> flame_tmp; + + bool confirm_rest = false; + bool try_others_rest = false; + bool skip_rest = false; + ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; + + auto addToTmp = [&](Mod* m, ModPlatform::Provider p) { + switch (p) { + case ModPlatform::Provider::MODRINTH: + modrinth_tmp.push_back(m); + break; + case ModPlatform::Provider::FLAME: + flame_tmp.push_back(m); + break; + } + }; + + for (auto candidate : m_candidates) { + auto* candidate_ptr = candidate.get(); + if (candidate->status() != ModStatus::NoMetadata) { + onMetadataEnsured(candidate_ptr); + continue; + } + + if (skip_rest) + continue; + + if (confirm_rest) { + addToTmp(candidate_ptr, provider_rest); + should_try_others.insert(candidate->internal_id(), try_others_rest); + continue; + } + + ChooseProviderDialog chooser(this); + chooser.setDescription(tr("The mod '%1' does not have a metadata yet. We need to generate it in order to track relevant " + "information on how to update this mod. " + "To do this, please select a mod provider which we can use to check for updates for this mod.") + .arg(candidate->name())); + auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted; + + auto response = chooser.getResponse(); + + if (response.skip_all) + skip_rest = true; + if (response.confirm_all) { + confirm_rest = true; + provider_rest = response.chosen; + try_others_rest = response.try_others; + } + + should_try_others.insert(candidate->internal_id(), response.try_others); + + if (confirmed) + addToTmp(candidate_ptr, response.chosen); + } + + if (!modrinth_tmp.empty()) { + auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH); + connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH); + }); + seq.addTask(modrinth_task); + } + + if (!flame_tmp.empty()) { + auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME); + connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME); + }); + seq.addTask(flame_task); + } + + seq.addTask(m_second_try_metadata); + + ProgressDialog checking_dialog(m_parent); + checking_dialog.setSkipButton(true, tr("Abort")); + checking_dialog.setWindowTitle(tr("Generating metadata...")); + auto ret_metadata = checking_dialog.execWithTask(&seq); + + return (ret_metadata != QDialog::DialogCode::Rejected); +} + +void ModUpdateDialog::onMetadataEnsured(Mod* mod) +{ + // When the mod is a folder, for instance + if (!mod->metadata()) + return; + + switch (mod->metadata()->provider) { + case ModPlatform::Provider::MODRINTH: + m_modrinth_to_update.push_back(mod); + break; + case ModPlatform::Provider::FLAME: + m_flame_to_update.push_back(mod); + break; + } +} + +ModPlatform::Provider next(ModPlatform::Provider p) +{ + switch (p) { + case ModPlatform::Provider::MODRINTH: + return ModPlatform::Provider::FLAME; + case ModPlatform::Provider::FLAME: + return ModPlatform::Provider::MODRINTH; + } + + return ModPlatform::Provider::FLAME; +} + +void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice) +{ + if (try_others) { + auto index_dir = indexDir(); + + auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice)); + connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); + + m_second_try_metadata->addTask(task); + } else { + QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; + + m_failed_metadata.append({mod, reason}); + } +} + +void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) +{ + auto item_top = new QTreeWidgetItem(ui->modTreeWidget); + item_top->setCheckState(0, Qt::CheckState::Checked); + item_top->setText(0, info.name); + item_top->setExpanded(true); + + auto provider_item = new QTreeWidgetItem(item_top); + provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider))); + + auto old_version_item = new QTreeWidgetItem(item_top); + old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version)); + + auto new_version_item = new QTreeWidgetItem(item_top); + new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); + + auto changelog_item = new QTreeWidgetItem(item_top); + changelog_item->setText(0, tr("Changelog of the latest version")); + + auto changelog = new QTreeWidgetItem(changelog_item); + auto changelog_area = new QTextBrowser(); + + switch (info.provider) { + case ModPlatform::Provider::MODRINTH: { + HoeDown h; + // HoeDown bug?: \n aren't converted to <br> + auto text = h.process(info.changelog.toUtf8()); + + // Don't convert if there's an HTML tag right after (Qt rendering weirdness) + text.remove(QRegularExpression("(\n+)(?=<)")); + text.replace('\n', "<br>"); + + changelog_area->setHtml(text); + break; + } + case ModPlatform::Provider::FLAME: { + changelog_area->setHtml(info.changelog); + break; + } + } + + changelog_area->setOpenExternalLinks(true); + changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::NoWrap); + changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + // HACK: Is there a better way of achieving this? + auto font_height = QFontMetrics(changelog_area->font()).height(); + changelog_area->setMaximumHeight((changelog_area->toPlainText().count(QRegularExpression("\n|<br>")) + 2) * font_height); + + ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); + + ui->modTreeWidget->addTopLevelItem(item_top); +} + +auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*> +{ + QList<ModDownloadTask*> list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 1; item != nullptr; ++i) { + if (item->checkState(0) == Qt::CheckState::Checked) { + list.push_back(m_tasks.find(item->text(0)).value()); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h new file mode 100644 index 00000000..76aaab36 --- /dev/null +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -0,0 +1,62 @@ +#pragma once + +#include "BaseInstance.h" +#include "ModDownloadTask.h" +#include "ReviewMessageBox.h" + +#include "minecraft/mod/ModFolderModel.h" + +#include "modplatform/CheckUpdateTask.h" + +class Mod; +class ModrinthCheckUpdate; +class FlameCheckUpdate; +class ConcurrentTask; + +class ModUpdateDialog final : public ReviewMessageBox { + Q_OBJECT + public: + explicit ModUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr<ModFolderModel> mod_model, + QList<Mod::Ptr>& search_for); + + void checkCandidates(); + + void appendMod(const CheckUpdateTask::UpdatableMod& info); + + const QList<ModDownloadTask*> getTasks(); + auto indexDir() const -> QDir { return m_mod_model->indexDir(); } + + auto noUpdates() const -> bool { return m_no_updates; }; + auto aborted() const -> bool { return m_aborted; }; + + private: + auto ensureMetadata() -> bool; + + private slots: + void onMetadataEnsured(Mod*); + void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); + + private: + QWidget* m_parent; + + ModrinthCheckUpdate* m_modrinth_check_task = nullptr; + FlameCheckUpdate* m_flame_check_task = nullptr; + + const std::shared_ptr<ModFolderModel> m_mod_model; + + QList<Mod::Ptr>& m_candidates; + QList<Mod*> m_modrinth_to_update; + QList<Mod*> m_flame_to_update; + + ConcurrentTask* m_second_try_metadata; + QList<std::tuple<Mod*, QString>> m_failed_metadata; + QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update; + + QHash<QString, ModDownloadTask*> m_tasks; + BaseInstance* m_instance; + + bool m_no_updates = false; + bool m_aborted = false; +}; 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..35bba9be 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); @@ -124,20 +150,21 @@ void NewInstanceDialog::accept() QList<BasePage *> NewInstanceDialog::getPages() { + QList<BasePage *> pages; + importPage = new ImportPage(this); - flamePage = new FlamePage(this); - auto technicPage = new TechnicPage(this); - return - { - new VanillaPage(this), - importPage, - new AtlPage(this), - flamePage, - new FtbPage(this), - new LegacyFTB::Page(this), - new ModrinthPage(this), - technicPage - }; + + pages.append(new VanillaPage(this)); + pages.append(importPage); + pages.append(new AtlPage(this)); + if (APPLICATION->currentCapabilities() & Application::SupportsFlame) + pages.append(new FlamePage(this)); + pages.append(new FtbPage(this)); + pages.append(new LegacyFTB::Page(this)); + pages.append(new ModrinthPage(this)); + pages.append(new TechnicPage(this)); + + return pages; } QString NewInstanceDialog::dialogTitle() diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h index ef74634e..a3c8cd1c 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.h +++ b/launcher/ui/dialogs/NewInstanceDialog.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 @@ -69,7 +89,6 @@ private: QString InstIconKey; ImportPage *importPage = nullptr; - FlamePage *flamePage = nullptr; std::unique_ptr<InstanceTask> creationTask; bool importIcon = false; diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp index df620464..d3b21627 100644 --- a/launcher/ui/dialogs/NewsDialog.cpp +++ b/launcher/ui/dialogs/NewsDialog.cpp @@ -16,7 +16,7 @@ NewsDialog::NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent) : QDialog(p m_article_list_hidden = ui->articleListWidget->isHidden(); auto first_item = ui->articleListWidget->item(0); - ui->articleListWidget->setItemSelected(first_item, true); + 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())); 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 e5226016..3c7f53d3 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -43,8 +43,8 @@ void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); - task->abort(); - QDialog::reject(); + if (task->abort()) + QDialog::reject(); } ProgressDialog::~ProgressDialog() @@ -62,24 +62,24 @@ void ProgressDialog::updateSize() int ProgressDialog::execWithTask(Task* task) { this->task = task; - QDialog::DialogCode result; if (!task) { - qDebug() << "Programmer error: progress dialog created with null task."; - return Accepted; + qDebug() << "Programmer error: Progress dialog created with null task."; + return QDialog::DialogCode::Accepted; } + QDialog::DialogCode result; if (handleImmediateResult(result)) { return result; } // Connect signals. - 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(stepStatus(QString)), SLOT(changeStatus(const QString&))); - connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + connect(task, &Task::started, this, &ProgressDialog::onTaskStarted); + connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed); + connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded); + connect(task, &Task::status, this, &ProgressDialog::changeStatus); + connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus); + connect(task, &Task::progress, this, &ProgressDialog::changeProgress); connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); @@ -89,19 +89,15 @@ int ProgressDialog::execWithTask(Task* task) ui->globalProgressBar->setHidden(true); } - // if this didn't connect to an already running task, invoke start + // It's a good idea to start the task after we entered the dialog's event loop :^) if (!task->isRunning()) { - task->start(); - } - if (task->isRunning()) { - changeProgress(task->getProgress(), task->getTotalProgress()); - changeStatus(task->getStatus()); - return QDialog::exec(); - } else if (handleImmediateResult(result)) { - return result; + QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection); } else { - return QDialog::Rejected; + changeStatus(task->getStatus()); + changeProgress(task->getProgress(), task->getTotalProgress()); } + + return QDialog::exec(); } // TODO: only provide the unique_ptr overloads diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index c92234a4..e664e566 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -40,7 +40,7 @@ auto ReviewMessageBox::deselectedMods() -> QStringList auto* item = ui->modTreeWidget->topLevelItem(0); - for (int i = 0; item != nullptr; ++i) { + for (int i = 1; item != nullptr; ++i) { if (item->checkState(0) == Qt::CheckState::Unchecked) { list.append(item->text(0)); } diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui index 299d2ecc..e684185f 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.ui +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> + <width>500</width> <height>455</height> </rect> </property> diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 8d137afc..8180ac1f 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,68 +57,72 @@ void SkinUploadDialog::on_buttonBox_accepted() { QString fileName; QString input = ui->skinPathTextBox->text(); - QRegExp urlPrefixMatcher("^([a-z]+)://.+$"); - bool isLocalFile = false; - // it has an URL prefix -> it is an URL - if(urlPrefixMatcher.exactMatch(input)) - { - QUrl fileURL = input; - if(fileURL.isValid()) + ProgressDialog prog(this); + SequentialTask skinUpload; + + if (!input.isEmpty()) { + QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$")); + bool isLocalFile = false; + // it has an URL prefix -> it is an URL + if(urlPrefixMatcher.match(input).hasMatch()) { - // local? - if(fileURL.isLocalFile()) + QUrl fileURL = input; + if(fileURL.isValid()) { - isLocalFile = true; - fileName = fileURL.toLocalFile(); + // local? + if(fileURL.isLocalFile()) + { + isLocalFile = true; + fileName = fileURL.toLocalFile(); + } + else + { + CustomMessageBox::selectable( + this, + tr("Skin Upload"), + tr("Using remote URLs for setting skins is not implemented yet."), + QMessageBox::Warning + )->exec(); + close(); + return; + } } else { CustomMessageBox::selectable( this, tr("Skin Upload"), - tr("Using remote URLs for setting skins is not implemented yet."), + tr("You cannot use an invalid URL for uploading skins."), QMessageBox::Warning - )->exec(); + )->exec(); close(); return; } } else { - CustomMessageBox::selectable( - this, - tr("Skin Upload"), - tr("You cannot use an invalid URL for uploading skins."), - QMessageBox::Warning - )->exec(); + // just assume it's a path then + isLocalFile = true; + fileName = ui->skinPathTextBox->text(); + } + if (isLocalFile && !QFile::exists(fileName)) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); close(); return; } + SkinUpload::Model model = SkinUpload::STEVE; + if (ui->steveBtn->isChecked()) + { + model = SkinUpload::STEVE; + } + else if (ui->alexBtn->isChecked()) + { + model = SkinUpload::ALEX; + } + skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); } - else - { - // just assume it's a path then - isLocalFile = true; - fileName = ui->skinPathTextBox->text(); - } - if (isLocalFile && !QFile::exists(fileName)) - { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); - close(); - return; - } - SkinUpload::Model model = SkinUpload::STEVE; - if (ui->steveBtn->isChecked()) - { - model = SkinUpload::STEVE; - } - else if (ui->alexBtn->isChecked()) - { - model = SkinUpload::ALEX; - } - ProgressDialog prog(this); - SequentialTask skinUpload; - skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); + auto selectedCape = ui->capeCombo->currentData().toString(); if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape))); diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui index f4b0ed0a..c7b16645 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.ui +++ b/launcher/ui/dialogs/SkinUploadDialog.ui @@ -21,7 +21,11 @@ </property> <layout class="QHBoxLayout" name="horizontalLayout"> <item> - <widget class="QLineEdit" name="skinPathTextBox"/> + <widget class="QLineEdit" name="skinPathTextBox"> + <property name="placeholderText"> + <string>Leave empty to keep current skin</string> + </property> + </widget> </item> <item> <widget class="QPushButton" name="skinBrowseBtn"> 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 b889e6f7..e3d30475 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -40,8 +40,10 @@ #include <QMessageBox> #include <QFileDialog> +#include <QRegularExpression> #include <QStandardPaths> #include <QTabBar> +#include <QValidator> #include <QVariant> #include "settings/SettingsObject.h" @@ -63,6 +65,10 @@ APIPage::APIPage(QWidget *parent) : }; static QRegularExpression validUrlRegExp("https?://.+"); + static QRegularExpression validMSAClientID(QRegularExpression::anchoredPattern( + "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")); + static QRegularExpression validFlameKey(QRegularExpression::anchoredPattern( + "\\$2[ayb]\\$.{56}")); ui->setupUi(this); @@ -75,6 +81,8 @@ APIPage::APIPage(QWidget *parent) : // 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->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); + ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); @@ -137,8 +145,8 @@ void APIPage::loadSettings() 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 flameKey = s->get("FlameKeyOverride").toString(); + ui->flameKey->setText(flameKey); QString customUserAgent = s->get("UserAgentOverride").toString(); ui->userAgentLineEdit->setText(customUserAgent); } @@ -167,8 +175,8 @@ void APIPage::applySettings() } s->set("MetaURLOverride", metaURL); - QString curseKey = ui->curseKey->text(); - s->set("CFKeyOverride", curseKey); + QString flameKey = ui->flameKey->text(); + s->set("FlameKeyOverride", flameKey); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); } diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 5327771c..1ae788c7 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -196,7 +196,7 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox_curse"> + <widget class="QGroupBox" name="groupBox_flame"> <property name="enabled"> <bool>true</bool> </property> @@ -214,7 +214,7 @@ <item row="2" column="0"> <widget class="QLabel" name="label_7"> <property name="text"> - <string>Enter a custom API Key for CurseForge here. </string> + <string>Enter a custom API Key for CurseForge here.</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -228,7 +228,7 @@ </widget> </item> <item row="1" column="0"> - <widget class="QLineEdit" name="curseKey"> + <widget class="QLineEdit" name="flameKey"> <property name="enabled"> <bool>true</bool> </property> diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index a608771e..fcc43add 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -96,7 +96,7 @@ AccountListPage::AccountListPage(QWidget *parent) updateButtonStates(); // Xbox authentication won't work without a client identifier, so disable the button if it is missing - if (APPLICATION->getMSAClientID().isEmpty()) { + if (~APPLICATION->currentCapabilities() & Application::SupportsMSA) { ui->actionAddMicrosoft->setVisible(false); ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set.")); } 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/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 4be24979..73ef0024 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -90,6 +90,13 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch { APPLICATION->updateChecker()->updateChanList(false); } + + if (APPLICATION->updateChecker()->getExternalUpdater()) + { + ui->updateChannelComboBox->setVisible(false); + ui->updateChannelDescLabel->setVisible(false); + ui->updateChannelLabel->setVisible(false); + } } else { @@ -266,7 +273,16 @@ void LauncherPage::applySettings() auto s = APPLICATION->settings(); // Updates - s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + { + APPLICATION->updateChecker()->getExternalUpdater()->setAutomaticallyChecksForUpdates( + ui->autoUpdateCheckBox->isChecked()); + } + else + { + s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + } + s->set("UpdateChannel", m_currentUpdateChannel); auto original = s->get("IconTheme").toString(); //FIXME: make generic @@ -351,7 +367,16 @@ void LauncherPage::loadSettings() { auto s = APPLICATION->settings(); // Updates - ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + { + ui->autoUpdateCheckBox->setChecked( + APPLICATION->updateChecker()->getExternalUpdater()->getAutomaticallyChecksForUpdates()); + } + else + { + ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + } + m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 417bbe05..645f7ef6 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -54,7 +54,7 @@ <item> <widget class="QCheckBox" name="autoUpdateCheckBox"> <property name="text"> - <string>Check for updates on start?</string> + <string>Check for updates automatically</string> </property> </widget> </item> 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 index 02eeae3d..69c20309 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -32,13 +32,13 @@ class SortProxy : public QSortFilterProxyModel { const auto& mod = model->at(source_row); - if (mod.name().contains(filterRegExp())) + if (filterRegularExpression().match(mod.name()).hasMatch()) return true; - if (mod.description().contains(filterRegExp())) + if (filterRegularExpression().match(mod.description()).hasMatch()) return true; for (auto& author : mod.authors()) { - if (author.contains(filterRegExp())) { + if (filterRegularExpression().match(author).hasMatch()) { return true; } } @@ -182,7 +182,7 @@ void ExternalResourcesPage::retranslate() void ExternalResourcesPage::filterTextChanged(const QString& newContents) { m_viewFilter = newContents; - m_filterModel->setFilterFixedString(m_viewFilter); + m_filterModel->setFilterRegularExpression(m_viewFilter); } void ExternalResourcesPage::runningStateChanged(bool running) diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 17bf455a..a13666b2 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -147,6 +147,17 @@ <string>Download a new resource</string> </property> </action> + <action name="actionUpdateItem"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Check for &Updates</string> + </property> + <property name="toolTip"> + <string>Try to check or update all selected resources (all resources if none are selected)</string> + </property> + </action> </widget> <customwidgets> <customwidget> diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 1d8cd1d7..f11cf992 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 @@ -349,7 +349,7 @@ void InstanceSettingsPage::loadSettings() ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool()); #if !defined(Q_OS_LINUX) - ui->perfomanceGroupBox->setVisible(false); + ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false); #endif // Miscellanous diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 8fefb44c..3d9fb025 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -63,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 4432ccc8..14e1f1e5 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -49,6 +49,7 @@ #include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/dialogs/ModUpdateDialog.h" #include "DesktopServices.h" @@ -78,6 +79,23 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); + ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); + + connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }); + + connect(mods.get(), &ModFolderModel::rowsInserted, this, + [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }); + + connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] { + ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); + + // Prevent a weird crash when trying to open the mods page twice in a session o.O + disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); + }); } } @@ -107,7 +125,6 @@ bool CoreModFolderPage::shouldDisplay() const return false; if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) return true; - } return false; } @@ -118,7 +135,7 @@ void ModFolderPage::installMods() return; 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!")); @@ -140,7 +157,7 @@ void ModFolderPage::installMods() QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - + tasks->deleteLater(); }); @@ -155,3 +172,63 @@ void ModFolderPage::installMods() m_model->update(); } } + +void ModFolderPage::updateMods() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedMods(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allMods(); + + ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All mods are up-to-date! :)"); + } else { + message = tr("All selected mods are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message) + ->exec(); + return; + } + + if (update_dialog.exec()) { + 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(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 1a9ed7db..0a7fc9fa 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 @@ -55,6 +56,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void installMods(); + void updateMods(); }; class CoreModFolderPage : public ModFolderPage { diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 51163e28..c97253e4 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -50,6 +50,7 @@ #include <QClipboard> #include <QKeyEvent> #include <QMenu> +#include <QRegularExpression> #include <Application.h> @@ -154,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) { @@ -270,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 3971d422..e5cce96c 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -288,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; @@ -306,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; @@ -615,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))); @@ -655,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/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 647b04a7..85cc01ff 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -211,7 +211,7 @@ void WorldListPage::on_actionDatapacks_triggered() return; } - if(!worldSafetyNagQuestion()) + if(!worldSafetyNagQuestion(tr("Open World Datapacks Folder"))) return; auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); @@ -269,7 +269,7 @@ void WorldListPage::on_actionMCEdit_triggered() return; } - if(!worldSafetyNagQuestion()) + if(!worldSafetyNagQuestion(tr("Open World in MCEdit"))) return; auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); @@ -373,11 +373,11 @@ bool WorldListPage::isWorldSafe(QModelIndex) return !m_inst->isRunning(); } -bool WorldListPage::worldSafetyNagQuestion() +bool WorldListPage::worldSafetyNagQuestion(const QString &actionType) { if(!isWorldSafe(getSelectedWorld())) { - auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?")); + auto result = QMessageBox::question(this, actionType, tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?")); if(result == QMessageBox::No) { return false; @@ -395,7 +395,7 @@ void WorldListPage::on_actionCopy_triggered() return; } - if(!worldSafetyNagQuestion()) + if(!worldSafetyNagQuestion(tr("Copy World"))) return; auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); @@ -417,7 +417,7 @@ void WorldListPage::on_actionRename_triggered() return; } - if(!worldSafetyNagQuestion()) + if(!worldSafetyNagQuestion(tr("Rename World"))) return; auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 17e36a08..1dc9e53e 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -93,7 +93,7 @@ protected: private: QModelIndex getSelectedWorld(); bool isWorldSafe(QModelIndex index); - bool worldSafetyNagQuestion(); + bool worldSafetyNagQuestion(const QString &actionType); void mceditError(); private: diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 0b8577b1..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 diff --git a/launcher/ui/pages/modplatform/ImportPage.ui b/launcher/ui/pages/modplatform/ImportPage.ui index 77bc5da5..0a50e871 100644 --- a/launcher/ui/pages/modplatform/ImportPage.ui +++ b/launcher/ui/pages/modplatform/ImportPage.ui @@ -40,7 +40,7 @@ <item> <widget class="QLabel" name="label_5"> <property name="text"> - <string>- Curseforge modpacks (ZIP)</string> + <string>- CurseForge modpacks (ZIP)</string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 1c160fd4..772fd2e0 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 @@ -64,7 +64,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { Q_UNUSED(loaders); - return ver.mcVersion.contains(mineVer); + return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); } // I don't know why, but doing this on the parent class makes it so that diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 86e1a17b..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 diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index 8a93bc2e..504d7f7b 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -126,7 +127,7 @@ void FtbPage::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this)); for(auto art : selected.art) { if(art.type == "square") { QString editedLogoName; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index c13b1554..2d135e59 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -168,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 7667d169..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(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index 0b81ea93..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 diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index c39acaa0..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 diff --git a/launcher/ui/themes/DarkTheme.cpp b/launcher/ui/themes/DarkTheme.cpp index 31ecd559..712a9d3e 100644 --- a/launcher/ui/themes/DarkTheme.cpp +++ b/launcher/ui/themes/DarkTheme.cpp @@ -31,6 +31,7 @@ QPalette DarkTheme::colorScheme() darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); darkPalette.setColor(QPalette::HighlightedText, Qt::black); + darkPalette.setColor(QPalette::PlaceholderText, Qt::darkGray); return fadeInactive(darkPalette, fadeAmount(), fadeColor()); } 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/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/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/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 8d5bd12d..79f1e0c9 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -76,13 +76,20 @@ void WideBar::addSeparator() m_entries.push_back(entry); } -void WideBar::insertActionBefore(QAction* before, QAction* action){ - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [before](BarEntry * entry) { - return entry->wideAction == before; +auto WideBar::getMatching(QAction* act) -> QList<BarEntry*>::iterator +{ + auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry * entry) { + return entry->wideAction == act; }); - if(iter == m_entries.end()) { + + return iter; +} + +void WideBar::insertActionBefore(QAction* before, QAction* action){ + auto iter = getMatching(before); + if(iter == m_entries.end()) return; - } + auto entry = new BarEntry(); entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this)); entry->wideAction = action; @@ -90,14 +97,24 @@ void WideBar::insertActionBefore(QAction* before, QAction* action){ m_entries.insert(iter, entry); } +void WideBar::insertActionAfter(QAction* after, QAction* action){ + auto iter = getMatching(after); + if(iter == m_entries.end()) + return; + + auto entry = new BarEntry(); + entry->qAction = insertWidget((*(iter+1))->qAction, new ActionButton(action, this)); + entry->wideAction = action; + entry->type = BarEntry::Action; + m_entries.insert(iter + 1, entry); +} + void WideBar::insertSpacer(QAction* action) { - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) { - return entry->wideAction == action; - }); - if(iter == m_entries.end()) { + auto iter = getMatching(action); + if(iter == m_entries.end()) return; - } + QWidget* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -107,6 +124,18 @@ void WideBar::insertSpacer(QAction* action) m_entries.insert(iter, entry); } +void WideBar::insertSeparator(QAction* before) +{ + auto iter = getMatching(before); + if(iter == m_entries.end()) + return; + + auto entry = new BarEntry(); + entry->qAction = QToolBar::insertSeparator(before); + entry->type = BarEntry::Separator; + m_entries.insert(iter, entry); +} + QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title) { QMenu *contextMenu = new QMenu(title, parent); diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index 2b676a8c..8ff62ef2 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -1,27 +1,34 @@ #pragma once -#include <QToolBar> #include <QAction> #include <QMap> +#include <QToolBar> class QMenu; -class WideBar : public QToolBar -{ +class WideBar : public QToolBar { Q_OBJECT -public: - explicit WideBar(const QString &title, QWidget * parent = nullptr); - explicit WideBar(QWidget * parent = nullptr); + public: + explicit WideBar(const QString& title, QWidget* parent = nullptr); + explicit WideBar(QWidget* parent = nullptr); virtual ~WideBar(); - void addAction(QAction *action); + void addAction(QAction* action); void addSeparator(); - void insertSpacer(QAction *action); - void insertActionBefore(QAction *before, QAction *action); - QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString()); -private: + void insertSpacer(QAction* action); + void insertSeparator(QAction* before); + void insertActionBefore(QAction* before, QAction* action); + void insertActionAfter(QAction* after, QAction* action); + + QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); + + private: struct BarEntry; - QList<BarEntry *> m_entries; + + auto getMatching(QAction* act) -> QList<BarEntry*>::iterator; + + private: + QList<BarEntry*> m_entries; }; diff --git a/launcher/updater/ExternalUpdater.h b/launcher/updater/ExternalUpdater.h new file mode 100644 index 00000000..a053e081 --- /dev/null +++ b/launcher/updater/ExternalUpdater.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.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/>. + */ + +#ifndef LAUNCHER_EXTERNALUPDATER_H +#define LAUNCHER_EXTERNALUPDATER_H + +#include <QObject> + +/*! + * A base class for an updater that uses an external library. + * This class contains basic functions to control the updater. + * + * To implement the updater on a new platform, create a new class that inherits from this class and + * implement the pure virtual functions. + * + * The initializer of the new class should have the side effect of starting the automatic updater. That is, + * once the class is initialized, the program should automatically check for updates if necessary. + */ +class ExternalUpdater : public QObject +{ + Q_OBJECT + +public: + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + virtual void checkForUpdates() = 0; + + /*! + * Indicates whether or not to check for updates automatically. + */ + virtual bool getAutomaticallyChecksForUpdates() = 0; + + /*! + * Indicates the current automatic update check interval in seconds. + */ + virtual double getUpdateCheckInterval() = 0; + + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + virtual bool getBetaAllowed() = 0; + + /*! + * Set whether or not to check for updates automatically. + */ + virtual void setAutomaticallyChecksForUpdates(bool check) = 0; + + /*! + * Set the current automatic update check interval in seconds. + */ + virtual void setUpdateCheckInterval(double seconds) = 0; + + /*! + * Set whether or not beta updates should be checked for in addition to regular releases. + */ + virtual void setBetaAllowed(bool allowed) = 0; + +signals: + /*! + * Emits whenever the user's ability to check for updates changes. + * + * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, + * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such + * as the feed or an update) is still being downloaded automatically in the background. + * + * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." + */ + void canCheckForUpdatesChanged(bool canCheck); +}; + +#endif //LAUNCHER_EXTERNALUPDATER_H diff --git a/launcher/updater/MacSparkleUpdater.h b/launcher/updater/MacSparkleUpdater.h new file mode 100644 index 00000000..d50dbd68 --- /dev/null +++ b/launcher/updater/MacSparkleUpdater.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.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/>. + */ + +#ifndef LAUNCHER_MACSPARKLEUPDATER_H +#define LAUNCHER_MACSPARKLEUPDATER_H + +#include <QObject> +#include <QSet> +#include "ExternalUpdater.h" + +/*! + * An implementation for the updater on macOS that uses the Sparkle framework. + */ +class MacSparkleUpdater : public ExternalUpdater +{ + Q_OBJECT + +public: + /*! + * Start the Sparkle updater, which automatically checks for updates if necessary. + */ + MacSparkleUpdater(); + ~MacSparkleUpdater() override; + + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + void checkForUpdates() override; + + /*! + * Indicates whether or not to check for updates automatically. + */ + bool getAutomaticallyChecksForUpdates() override; + + /*! + * Indicates the current automatic update check interval in seconds. + */ + double getUpdateCheckInterval() override; + + /*! + * Indicates the set of Sparkle channels the updater is allowed to find new updates from. + */ + QSet<QString> getAllowedChannels(); + + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + bool getBetaAllowed() override; + + /*! + * Set whether or not to check for updates automatically. + * + * As per Sparkle documentation, "By default, Sparkle asks users on second launch for permission if they want + * automatic update checks enabled and sets this property based on their response. If SUEnableAutomaticChecks is + * set in the Info.plist, this permission request is not performed however. + * + * Setting this property will persist in the host bundle’s user defaults. Only set this property if you need + * dynamic behavior (e.g. user preferences). + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setAutomaticallyChecksForUpdates(bool check) override; + + /*! + * Set the current automatic update check interval in seconds. + * + * As per Sparkle documentation, "Setting this property will persist in the host bundle’s user defaults. For this + * reason, only set this property if you need dynamic behavior (eg user preferences). Otherwise prefer to set + * SUScheduledCheckInterval directly in your Info.plist. + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setUpdateCheckInterval(double seconds) override; + + /*! + * Clears all allowed Sparkle channels, returning to the default updater channel behavior. + */ + void clearAllowedChannels(); + + /*! + * Set a single Sparkle channel the updater is allowed to find new updates from. + * + * Items in the default channel can always be found, regardless of this setting. If an empty string is passed, + * return to the default behavior. + */ + void setAllowedChannel(const QString& channel); + + /*! + * Set a set of Sparkle channels the updater is allowed to find new updates from. + * + * Items in the default channel can always be found, regardless of this setting. If an empty set is passed, + * return to the default behavior. + */ + void setAllowedChannels(const QSet<QString>& channels); + + /*! + * Set whether or not beta updates should be checked for in addition to regular releases. + */ + void setBetaAllowed(bool allowed) override; + +private: + class Private; + + Private *priv; + + void loadChannelsFromSettings(); +}; + +#endif //LAUNCHER_MACSPARKLEUPDATER_H diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm new file mode 100644 index 00000000..ca6da55a --- /dev/null +++ b/launcher/updater/MacSparkleUpdater.mm @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.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 "MacSparkleUpdater.h" + +#include "Application.h" + +#include <Cocoa/Cocoa.h> +#include <Sparkle/Sparkle.h> + +@interface UpdaterObserver : NSObject + +@property(nonatomic, readonly) SPUUpdater* updater; + +/// A callback to run when the state of `canCheckForUpdates` for the `updater` changes. +@property(nonatomic, copy) void (^callback) (bool); + +- (id)initWithUpdater:(SPUUpdater*)updater; + +@end + +@implementation UpdaterObserver + +- (id)initWithUpdater:(SPUUpdater*)updater +{ + self = [super init]; + _updater = updater; + [self addObserver:self forKeyPath:@"updater.canCheckForUpdates" options:NSKeyValueObservingOptionNew context:nil]; + + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary<NSKeyValueChangeKey, id> *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"updater.canCheckForUpdates"]) + { + bool canCheck = [change[NSKeyValueChangeNewKey] boolValue]; + self.callback(canCheck); + } +} + +@end + + +@interface UpdaterDelegate : NSObject <SPUUpdaterDelegate> + +@property(nonatomic, copy) NSSet<NSString *> *allowedChannels; + +@end + +@implementation UpdaterDelegate + +- (NSSet<NSString *> *)allowedChannelsForUpdater:(SPUUpdater *)updater +{ + return _allowedChannels; +} + +@end + + +class MacSparkleUpdater::Private +{ +public: + SPUStandardUpdaterController *updaterController; + UpdaterObserver *updaterObserver; + UpdaterDelegate *updaterDelegate; + NSAutoreleasePool *autoReleasePool; +}; + +MacSparkleUpdater::MacSparkleUpdater() +{ + priv = new MacSparkleUpdater::Private(); + + // Enable Cocoa's memory management. + NSApplicationLoad(); + priv->autoReleasePool = [[NSAutoreleasePool alloc] init]; + + // Delegate is used for setting/getting allowed update channels. + priv->updaterDelegate = [[UpdaterDelegate alloc] init]; + + // Controller is the interface for actually doing the updates. + priv->updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:true + updaterDelegate:priv->updaterDelegate + userDriverDelegate:nil]; + + priv->updaterObserver = [[UpdaterObserver alloc] initWithUpdater:priv->updaterController.updater]; + // Use KVO to run a callback that emits a Qt signal when `canCheckForUpdates` changes, so the UI can respond accordingly. + priv->updaterObserver.callback = ^(bool canCheck) { + emit canCheckForUpdatesChanged(canCheck); + }; + + loadChannelsFromSettings(); +} + +MacSparkleUpdater::~MacSparkleUpdater() +{ + [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"]; + + [priv->updaterController release]; + [priv->updaterObserver release]; + [priv->updaterDelegate release]; + [priv->autoReleasePool release]; + delete priv; +} + +void MacSparkleUpdater::checkForUpdates() +{ + [priv->updaterController checkForUpdates:nil]; +} + +bool MacSparkleUpdater::getAutomaticallyChecksForUpdates() +{ + return priv->updaterController.updater.automaticallyChecksForUpdates; +} + +double MacSparkleUpdater::getUpdateCheckInterval() +{ + return priv->updaterController.updater.updateCheckInterval; +} + +QSet<QString> MacSparkleUpdater::getAllowedChannels() +{ + // Convert NSSet<NSString> -> QSet<QString> + __block QSet<QString> channels; + [priv->updaterDelegate.allowedChannels enumerateObjectsUsingBlock:^(NSString *channel, BOOL *stop) + { + channels.insert(QString::fromNSString(channel)); + }]; + return channels; +} + +bool MacSparkleUpdater::getBetaAllowed() +{ + return getAllowedChannels().contains("beta"); +} + +void MacSparkleUpdater::setAutomaticallyChecksForUpdates(bool check) +{ + priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy +} + +void MacSparkleUpdater::setUpdateCheckInterval(double seconds) +{ + priv->updaterController.updater.updateCheckInterval = seconds; +} + +void MacSparkleUpdater::clearAllowedChannels() +{ + priv->updaterDelegate.allowedChannels = [NSSet set]; + APPLICATION->settings()->set("UpdateChannel", ""); +} + +void MacSparkleUpdater::setAllowedChannel(const QString &channel) +{ + if (channel.isEmpty()) + { + clearAllowedChannels(); + return; + } + + NSSet<NSString *> *nsChannels = [NSSet setWithObject:channel.toNSString()]; + priv->updaterDelegate.allowedChannels = nsChannels; + APPLICATION->settings()->set("UpdateChannel", channel); +} + +void MacSparkleUpdater::setAllowedChannels(const QSet<QString> &channels) +{ + if (channels.isEmpty()) + { + clearAllowedChannels(); + return; + } + + QString channelsConfig = ""; + // Convert QSet<QString> -> NSSet<NSString> + NSMutableSet<NSString *> *nsChannels = [NSMutableSet setWithCapacity:channels.count()]; + foreach (const QString channel, channels) + { + [nsChannels addObject:channel.toNSString()]; + channelsConfig += channel + " "; + } + + priv->updaterDelegate.allowedChannels = nsChannels; + APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed()); +} + +void MacSparkleUpdater::setBetaAllowed(bool allowed) +{ + if (allowed) + { + setAllowedChannel("beta"); + } + else + { + clearAllowedChannels(); + } +} + +void MacSparkleUpdater::loadChannelsFromSettings() +{ + QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" "); + QSet<QString> channels(channelList.begin(), channelList.end()); + setAllowedChannels(channels); +} diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index efdb6093..fa6e5a97 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -24,7 +24,6 @@ #define CHANLIST_FORMAT 0 #include "BuildConfig.h" -#include "sys.h" UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild) { @@ -32,6 +31,10 @@ UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QStr m_channelUrl = channelUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; + +#ifdef Q_OS_MAC + m_externalUpdater = new MacSparkleUpdater(); +#endif } QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const @@ -44,71 +47,95 @@ bool UpdateChecker::hasChannels() const return !m_channels.isEmpty(); } -void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) +ExternalUpdater* UpdateChecker::getExternalUpdater() { - qDebug() << "Checking for updates."; - - // If the channel list hasn't loaded yet, load it and defer checking for updates until - // later. - if (!m_chanListLoaded) - { - qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; - m_checkUpdateWaiting = true; - m_deferredUpdateChannel = updateChannel; - updateChanList(notifyNoUpdate); - return; - } + return m_externalUpdater; +} - if (m_updateChecking) +void UpdateChecker::checkForUpdate(const QString& updateChannel, bool notifyNoUpdate) +{ + if (m_externalUpdater) { - qDebug() << "Ignoring update check request. Already checking for updates."; - return; + m_externalUpdater->setBetaAllowed(updateChannel == "beta"); + if (notifyNoUpdate) + { + qDebug() << "Checking for updates."; + m_externalUpdater->checkForUpdates(); + } else + { + // The updater library already handles automatic update checks. + return; + } } - - // Find the desired channel within the channel list and get its repo URL. If if cannot be - // found, error. - QString stableUrl; - m_newRepoUrl = ""; - for (ChannelListEntry entry : m_channels) + else { - qDebug() << "channelEntry = " << entry.id; - if(entry.id == "stable") { - stableUrl = entry.url; + qDebug() << "Checking for updates."; + // If the channel list hasn't loaded yet, load it and defer checking for updates until + // later. + if (!m_chanListLoaded) + { + qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; + m_checkUpdateWaiting = true; + m_deferredUpdateChannel = updateChannel; + updateChanList(notifyNoUpdate); + return; } - if (entry.id == updateChannel) { - m_newRepoUrl = entry.url; - qDebug() << "is intended update channel: " << entry.id; + + if (m_updateChecking) + { + qDebug() << "Ignoring update check request. Already checking for updates."; + return; } - if (entry.id == m_currentChannel) { - m_currentRepoUrl = entry.url; - qDebug() << "is current update channel: " << entry.id; + + // Find the desired channel within the channel list and get its repo URL. If if cannot be + // found, error. + QString stableUrl; + m_newRepoUrl = ""; + for (ChannelListEntry entry: m_channels) + { + qDebug() << "channelEntry = " << entry.id; + if (entry.id == "stable") + { + stableUrl = entry.url; + } + if (entry.id == updateChannel) + { + m_newRepoUrl = entry.url; + qDebug() << "is intended update channel: " << entry.id; + } + if (entry.id == m_currentChannel) + { + m_currentRepoUrl = entry.url; + qDebug() << "is current update channel: " << entry.id; + } } - } - qDebug() << "m_repoUrl = " << m_newRepoUrl; + qDebug() << "m_repoUrl = " << m_newRepoUrl; - if (m_newRepoUrl.isEmpty()) { - qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; - m_newRepoUrl = stableUrl; - } + if (m_newRepoUrl.isEmpty()) + { + qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; + m_newRepoUrl = stableUrl; + } - // If nothing applies, error - if (m_newRepoUrl.isEmpty()) - { - qCritical() << "failed to select any update repository for: " << updateChannel; - emit updateCheckFailed(); - return; - } + // If nothing applies, error + if (m_newRepoUrl.isEmpty()) + { + qCritical() << "failed to select any update repository for: " << updateChannel; + emit updateCheckFailed(); + return; + } - m_updateChecking = true; + m_updateChecking = true; - QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); + QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); - indexJob = new NetJob("GoUpdate Repository Index", m_network); - indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); - connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); - connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); - indexJob->start(); + indexJob = new NetJob("GoUpdate Repository Index", m_network); + indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); + connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { updateCheckFinished(notifyNoUpdate); }); + connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); + indexJob->start(); + } } void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h index 13ee4efd..94e4312b 100644 --- a/launcher/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -17,6 +17,11 @@ #include "net/NetJob.h" #include "GoUpdate.h" +#include "ExternalUpdater.h" + +#ifdef Q_OS_MAC +#include "MacSparkleUpdater.h" +#endif class UpdateChecker : public QObject { @@ -24,7 +29,7 @@ class UpdateChecker : public QObject public: UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild); - void checkForUpdate(QString updateChannel, bool notifyNoUpdate); + void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate); /*! * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). @@ -54,6 +59,11 @@ public: */ bool hasChannels() const; + /*! + * Returns a pointer to an object that controls the external updater, or nullptr if an external updater is not used. + */ + ExternalUpdater *getExternalUpdater(); + signals: //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version. void updateAvailable(GoUpdate::Status status); @@ -117,5 +127,14 @@ private: QString m_currentRepoUrl; QString m_newRepoUrl; + + /*! + * If not a nullptr, then the updater here will be used instead of the old updater that uses GoUpdate when + * checking for updates. + * + * As a result, signals from this class won't be emitted, and most of the functions in this class other + * than checkForUpdate are not useful. Call functions from this external updater object instead. + */ + ExternalUpdater *m_externalUpdater = nullptr; }; |