diff options
119 files changed, 3150 insertions, 934 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b387f46a..bac73932 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -27,7 +27,7 @@ body: attributes: label: Version of PolyMC description: The version of PolyMC used in the bug report. - placeholder: PolyMC 1.2.2 + placeholder: PolyMC 1.3.2 validations: required: true - type: textarea diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml deleted file mode 100644 index fa287a2c..00000000 --- a/.github/workflows/backport.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Backport PR to stable -on: - pull_request: - branches: [ develop ] - types: [ closed ] -jobs: - release_pull_request: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'backport') - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Backport PR by cherry-pick-ing - uses: Nathanmalnoury/gh-backport-action@master - with: - pr_branch: 'stable' - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db7bd653..d8fc1ff2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: name: "Windows-x86_64" msystem: mingw64 - - os: macos-11 + - os: macos-12 macosx_deployment_target: 10.13 runs-on: ${{ matrix.os }} diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml deleted file mode 100644 index f0f5b8cc..00000000 --- a/.github/workflows/pr-comment.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Comment on pull request -on: - workflow_run: - workflows: ['Build Application'] - types: [completed] -jobs: - pr_comment: - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v5 - with: - # This snippet is public-domain, taken from - # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml - script: | - async function upsertComment(owner, repo, issue_number, purpose, body) { - const {data: comments} = await github.rest.issues.listComments( - {owner, repo, issue_number}); - - const marker = `<!-- bot: ${purpose} -->`; - body = marker + "\n" + body; - - const existing = comments.filter((c) => c.body.includes(marker)); - if (existing.length > 0) { - const last = existing[existing.length - 1]; - core.info(`Updating comment ${last.id}`); - await github.rest.issues.updateComment({ - owner, repo, - body, - comment_id: last.id, - }); - } else { - core.info(`Creating a comment in issue / PR #${issue_number}`); - await github.rest.issues.createComment({issue_number, body, owner, repo}); - } - } - - const {owner, repo} = context.repo; - const run_id = ${{github.event.workflow_run.id}}; - - const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }}; - if (!pull_requests.length) { - return core.error("This workflow doesn't match any pull requests!"); - } - - const artifacts = await github.paginate( - github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id}); - if (!artifacts.length) { - return core.error(`No artifacts found`); - } - let body = `Download the artifacts for this pull request:\n`; - for (const art of artifacts) { - body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; - } - - core.info("Review thread message body:", body); - - for (const pr of pull_requests) { - await upsertComment(owner, repo, pr.number, - "nightly-link", body); - } diff --git a/CMakeLists.txt b/CMakeLists.txt index 11d58213..b09e7fd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,8 +45,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") # set CXXFLAGS for build targets -set(CMAKE_CXX_FLAGS_DEBUG "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") -set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") option(ENABLE_LTO "Enable Link Time Optimization" off) @@ -73,8 +72,8 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) -set(Launcher_VERSION_MINOR 3) -set(Launcher_VERSION_HOTFIX 1) +set(Launcher_VERSION_MINOR 4) +set(Launcher_VERSION_HOTFIX 0) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") @@ -47,7 +47,7 @@ If there are any issues with the space or you are using a client that does not s [](https://matrix.to/#/#polymc-support:matrix.org) [](https://matrix.to/#/#polymc-voice:matrix.org) -we also have a subreddit you can post your issues and suggestions on: +We also have a subreddit you can post your issues and suggestions on: [r/PolyMCLauncher](https://www.reddit.com/r/PolyMCLauncher/) @@ -80,13 +80,31 @@ To modify download information or change packaging information send a pull reque ## Forking/Redistributing/Custom builds policy -Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. +We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy: +- Make it clear that your fork is not PolyMC and is not endorsed by or affiliated with the PolyMC project (https://polymc.org). +- Go through [CMakeLists.txt](CMakeLists.txt) and change PolyMC's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). + +If you have any questions or want any clarification on the above conditions please make an issue and ask us. Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) -If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file. + +If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). All launcher code is available under the GPL-3.0-only license. The logo and related assets are under the CC BY-SA 4.0 license. + +## Sponsors +Thank you to all our generous backers over at Open Collective! Support PolyMC by [becoming a backer](https://opencollective.com/polymc). + +[](https://opencollective.com/polymc#backers) + +Also, thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). + +[](https://www.jetbrains.com/opensource/) + +Additionally, thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), for providing M1-Macs for development purposes! + +<a href="https://www.macstadium.com"><img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="Powered by MacStadium" width="300"></a> @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1653326962, - "narHash": "sha256-W8feCYqKTsMre4nAEpv5Kx1PVFC+hao/LwqtB2Wci/8=", + "lastModified": 1654665288, + "narHash": "sha256-7blJpfoZEu7GKb84uh3io/5eSJNdaagXD9d15P9iQMs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "41cc1d5d9584103be4108c1815c350e07c807036", + "rev": "43ecbe7840d155fa933ee8a500fb00dbbc651fc8", "type": "github" }, "original": { @@ -22,15 +22,17 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { - packages = forAllSystems (system: { + packages = forAllSystems (system: rec { polymc = pkgs.${system}.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; polymc-qt6 = pkgs.${system}.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; + + default = polymc; }); - defaultPackage = forAllSystems (system: self.packages.${system}.polymc); + defaultPackage = forAllSystems (system: self.packages.${system}.default); - apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; }); - defaultApp = forAllSystems (system: self.apps.${system}.polymc); + apps = forAllSystems (system: rec { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; default = polymc; }); + defaultApp = forAllSystems (system: self.apps.${system}.default); overlay = final: prev: { polymc = self.defaultPackage.${final.system}; }; }; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ba4096b6..ab3110a3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -154,6 +154,7 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt fflush(stderr); } +#ifdef LAUNCHER_WITH_UPDATER QString getIdealPlatform(QString currentPlatform) { auto info = Sys::getKernelInfo(); switch(info.kernelType) { @@ -192,6 +193,7 @@ QString getIdealPlatform(QString currentPlatform) { } return currentPlatform; } +#endif } @@ -643,6 +645,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); + // Minecraft mods + m_settings->registerSetting("ModMetadataDisabled", false); + // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); @@ -708,6 +713,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); m_settings->registerSetting("CFKeyOverride", ""); + m_settings->registerSetting("UserAgentOverride", ""); // Init page provider { @@ -750,6 +756,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "<> Translations loaded."; } +#ifdef LAUNCHER_WITH_UPDATER // initialize the updater if(BuildConfig.UPDATER_ENABLED) { @@ -759,6 +766,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); qDebug() << "<> Updater started."; } +#endif // Instance icons { @@ -871,6 +879,12 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_mcedit.reset(new MCEditTool(m_settings)); } +#ifdef Q_OS_MACOS + connect(this, &Application::clickedOnDock, [this]() { + this->showMainWindow(); + }); +#endif + connect(this, &Application::aboutToQuit, [this](){ if(m_instances) { @@ -954,6 +968,21 @@ bool Application::createSetupWizard() return false; } +bool Application::event(QEvent* event) { +#ifdef Q_OS_MACOS + if (event->type() == QEvent::ApplicationStateChange) { + auto ev = static_cast<QApplicationStateChangeEvent*>(event); + + if (m_prevAppState == Qt::ApplicationActive + && ev->applicationState() == Qt::ApplicationActive) { + emit clickedOnDock(); + } + m_prevAppState = ev->applicationState(); + } +#endif + return QApplication::event(event); +} + void Application::setupWizardFinished(int status) { qDebug() << "Wizard result =" << status; @@ -1383,7 +1412,9 @@ MainWindow* Application::showMainWindow(bool minimized) } m_mainWindow->checkInstancePathForProblems(); +#ifdef LAUNCHER_WITH_UPDATER connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged); +#endif connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); m_openWindows++; } @@ -1553,3 +1584,24 @@ QString Application::getCurseKey() return BuildConfig.CURSEFORGE_API_KEY; } + +QString Application::getUserAgent() +{ + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); + } + + return BuildConfig.USER_AGENT; +} + +QString Application::getUserAgentUncached() +{ + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + uaOverride += " (Uncached)"; + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); + } + + return BuildConfig.USER_AGENT_UNCACHED; +} diff --git a/launcher/Application.h b/launcher/Application.h index 3129b4fb..09007160 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -42,7 +42,10 @@ #include <QIcon> #include <QDateTime> #include <QUrl> + +#ifdef LAUNCHER_WITH_UPDATER #include <updater/GoUpdate.h> +#endif #include <BaseInstance.h> @@ -94,6 +97,8 @@ public: Application(int &argc, char **argv); virtual ~Application(); + bool event(QEvent* event) override; + std::shared_ptr<SettingsObject> settings() const { return m_settings; } @@ -156,6 +161,8 @@ public: QString getMSAClientID(); QString getCurseKey(); + QString getUserAgent(); + QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString &root() { @@ -181,6 +188,10 @@ signals: void globalSettingsAboutToOpen(); void globalSettingsClosed(); +#ifdef Q_OS_MACOS + void clickedOnDock(); +#endif + public slots: bool launch( InstancePtr instance, @@ -238,6 +249,10 @@ private: QString m_rootPath; Status m_status = Application::StartingUp; +#ifdef Q_OS_MACOS + Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; +#endif + #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 2fb31d94..0efbdddc 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -59,7 +60,11 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); - m_settings->registerSetting("InstanceType", "OneSix"); + + // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of + // a locally stored instance + if (!m_settings->getSetting("InstanceType")) + m_settings->registerSetting("InstanceType", ""); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); @@ -76,6 +81,14 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); + + // Managed Packs + m_settings->registerSetting("ManagedPack", false); + m_settings->registerSetting("ManagedPackType", ""); + m_settings->registerSetting("ManagedPackID", ""); + m_settings->registerSetting("ManagedPackName", ""); + m_settings->registerSetting("ManagedPackVersionID", ""); + m_settings->registerSetting("ManagedPackVersionName", ""); } QString BaseInstance::getPreLaunchCommand() @@ -93,6 +106,46 @@ QString BaseInstance::getPostExitCommand() return settings()->get("PostExitCommand").toString(); } +bool BaseInstance::isManagedPack() +{ + return settings()->get("ManagedPack").toBool(); +} + +QString BaseInstance::getManagedPackType() +{ + return settings()->get("ManagedPackType").toString(); +} + +QString BaseInstance::getManagedPackID() +{ + return settings()->get("ManagedPackID").toString(); +} + +QString BaseInstance::getManagedPackName() +{ + return settings()->get("ManagedPackName").toString(); +} + +QString BaseInstance::getManagedPackVersionID() +{ + return settings()->get("ManagedPackVersionID").toString(); +} + +QString BaseInstance::getManagedPackVersionName() +{ + return settings()->get("ManagedPackVersionName").toString(); +} + +void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) +{ + settings()->set("ManagedPack", true); + settings()->set("ManagedPackType", type); + settings()->set("ManagedPackID", id); + settings()->set("ManagedPackName", name); + settings()->set("ManagedPackVersionID", versionId); + settings()->set("ManagedPackVersionName", version); +} + int BaseInstance::getConsoleMaxLines() const { auto lineSetting = settings()->getSetting("ConsoleMaxLines"); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index c973fcd4..66177614 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -139,6 +140,14 @@ public: QString getPostExitCommand(); QString getWrapperCommand(); + bool isManagedPack(); + QString getManagedPackType(); + QString getManagedPackID(); + QString getManagedPackName(); + QString getManagedPackVersionID(); + QString getManagedPackVersionName(); + void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); + /// guess log level from a line of game log virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) { diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index bbf80185..b8db803b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -156,27 +156,29 @@ set(LAUNCH_SOURCES launch/LogModel.h ) -# Old update system -set(UPDATE_SOURCES - updater/GoUpdate.h - updater/GoUpdate.cpp - updater/UpdateChecker.h - updater/UpdateChecker.cpp - updater/DownloadTask.h - updater/DownloadTask.cpp -) - -add_unit_test(UpdateChecker - SOURCES updater/UpdateChecker_test.cpp - LIBS Launcher_logic - DATA updater/testdata +if (Launcher_UPDATER_BASE) + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_WITH_UPDATER ${Launcher_APP_BINARY_DEFS}") + # Old update system + set(UPDATE_SOURCES + updater/GoUpdate.h + updater/GoUpdate.cpp + updater/UpdateChecker.h + updater/UpdateChecker.cpp + updater/DownloadTask.h + updater/DownloadTask.cpp ) -add_unit_test(DownloadTask - SOURCES updater/DownloadTask_test.cpp - LIBS Launcher_logic - DATA updater/testdata + add_unit_test(UpdateChecker + SOURCES updater/UpdateChecker_test.cpp + LIBS Launcher_logic + DATA updater/testdata + ) + add_unit_test(DownloadTask + SOURCES updater/DownloadTask_test.cpp + LIBS Launcher_logic + DATA updater/testdata ) +endif() # Backend for the news bar... there's usually no news. set(NEWS_SOURCES @@ -324,19 +326,22 @@ set(MINECRAFT_SOURCES minecraft/WorldList.h minecraft/WorldList.cpp + minecraft/mod/MetadataHandler.h minecraft/mod/Mod.h minecraft/mod/Mod.cpp minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp - minecraft/mod/ModFolderLoadTask.h - minecraft/mod/ModFolderLoadTask.cpp - minecraft/mod/LocalModParseTask.h - minecraft/mod/LocalModParseTask.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/tasks/ModFolderLoadTask.h + minecraft/mod/tasks/ModFolderLoadTask.cpp + minecraft/mod/tasks/LocalModParseTask.h + minecraft/mod/tasks/LocalModParseTask.cpp + minecraft/mod/tasks/LocalModUpdateTask.h + minecraft/mod/tasks/LocalModUpdateTask.cpp # Assets minecraft/AssetsUtils.h @@ -499,6 +504,9 @@ set(META_SOURCES ) set(API_SOURCES + modplatform/ModIndex.h + modplatform/ModIndex.cpp + modplatform/ModAPI.h modplatform/flame/FlameAPI.h @@ -545,6 +553,17 @@ set(MODPACKSCH_SOURCES modplatform/modpacksch/FTBPackManifest.cpp ) +set(PACKWIZ_SOURCES + modplatform/packwiz/Packwiz.h + modplatform/packwiz/Packwiz.cpp +) + +add_unit_test(Packwiz + SOURCES modplatform/packwiz/Packwiz_test.cpp + DATA modplatform/packwiz/testdata + LIBS Launcher_logic + ) + set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.cpp @@ -598,6 +617,7 @@ set(LOGIC_SOURCES ${FLAME_SOURCES} ${MODRINTH_SOURCES} ${MODPACKSCH_SOURCES} + ${PACKWIZ_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} ) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 6de20de6..3837d75f 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -454,4 +454,47 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na return false; #endif } + +QStringList listFolderPaths(QDir root) +{ + auto createAbsPath = [](QFileInfo const& entry) { return FS::PathCombine(entry.path(), entry.fileName()); }; + + QStringList entries; + + root.refresh(); + for (auto entry : root.entryInfoList(QDir::Filter::Files)) { + entries.append(createAbsPath(entry)); + } + + for (auto entry : root.entryInfoList(QDir::Filter::AllDirs | QDir::Filter::NoDotAndDotDot)) { + entries.append(listFolderPaths(createAbsPath(entry))); + } + + return entries; +} + +bool overrideFolder(QString overwritten_path, QString override_path) +{ + if (!FS::ensureFolderPathExists(overwritten_path)) + return false; + + QStringList paths_to_override; + QDir root_override (override_path); + for (auto file : listFolderPaths(root_override)) { + QString destination = file; + destination.replace(override_path, overwritten_path); + + qDebug() << QString("Applying override %1 in %2").arg(file, destination); + + if (QFile::exists(destination)) + QFile::remove(destination); + if (!QFile::rename(file, destination)) { + qCritical() << QString("Failed to apply override from %1 to %2").arg(file, destination); + return false; + } + } + + return true; +} + } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 8f6e8b48..bc942ab3 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -124,4 +124,8 @@ QString getDesktopDir(); // call it *name* and assign it the icon *icon* // return true if operation succeeded bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation); + +// Overrides one folder with the contents of another, preserving items exclusive to the first folder +// Equivalent to doing QDir::rename, but allowing for overrides +bool overrideFolder(QString overwritten_path, QString override_path); } diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 09c2a333..d5684805 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -582,10 +582,10 @@ void InstanceImportTask::processMultiMC() emitSucceeded(); } +// https://docs.modrinth.com/docs/modpacks/format_definition/ void InstanceImportTask::processModrinth() { std::vector<Modrinth::File> files; - std::vector<Modrinth::File> non_whitelisted_files; QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -600,26 +600,30 @@ void InstanceImportTask::processModrinth() auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); bool had_optional = false; - for (auto& modInfo : jsonFiles) { + for (auto modInfo : jsonFiles) { Modrinth::File file; file.path = Json::requireString(modInfo, "path"); auto env = Json::ensureObject(modInfo, "env"); - QString support = Json::ensureString(env, "client", "unsupported"); - if (support == "unsupported") { - continue; - } else if (support == "optional") { - // TODO: Make a review dialog for choosing which ones the user wants! - if (!had_optional) { - had_optional = true; - auto info = CustomMessageBox::selectable( - m_parent, tr("Optional mod detected!"), - tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), QMessageBox::Information); - info->exec(); - } + // 'env' field is optional + if (!env.isEmpty()) { + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") { + continue; + } else if (support == "optional") { + // TODO: Make a review dialog for choosing which ones the user wants! + if (!had_optional) { + had_optional = true; + auto info = CustomMessageBox::selectable( + m_parent, tr("Optional mod detected!"), + tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), + QMessageBox::Information); + info->exec(); + } - if (file.path.endsWith(".jar")) - file.path += ".disabled"; + if (file.path.endsWith(".jar")) + file.path += ".disabled"; + } } QJsonObject hashes = Json::requireObject(modInfo, "hashes"); @@ -640,40 +644,31 @@ void InstanceImportTask::processModrinth() } file.hash = QByteArray::fromHex(hash.toLatin1()); file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) - - file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path); - - if (!file.download.isValid()) { - qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(file.download.toString(), file.path); - throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); - } - else if (!Modrinth::validateDownloadUrl(file.download)) { - qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(file.download.toString(), file.path); - non_whitelisted_files.push_back(file); + + auto download_arr = Json::ensureArray(modInfo, "downloads"); + for(auto download : download_arr) { + qWarning() << download.toString(); + bool is_last = download.toString() == download_arr.last().toString(); + + auto download_url = QUrl(download.toString()); + + if (!download_url.isValid()) { + qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL") + .arg(download_url.toString(), file.path); + if(is_last && file.downloads.isEmpty()) + throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + } + else { + file.downloads.push_back(download_url); + } } files.push_back(file); } - if (!non_whitelisted_files.empty()) { - QString text; - for (const auto& file : non_whitelisted_files) { - text += tr("Filepath: %1<br>URL: <a href='%2'>%2</a><br>").arg(file.path, file.download.toString()); - } - - auto message_dialog = new ScrollMessageBox(m_parent, tr("Non-whitelisted mods found"), - tr("The following mods have URLs that are not whitelisted by Modrinth.\n" - "Proceed with caution!"), - text); - message_dialog->setModal(true); - if (message_dialog->exec() == QDialog::Rejected) { - emitFailed("Aborted"); - return; - } - } - auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); @@ -701,16 +696,26 @@ void InstanceImportTask::processModrinth() emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } + + auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); - if (QFile::exists(overridePath)) { - QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - if (!QFile::rename(overridePath, mcPath)) { + auto override_path = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(override_path)) { + if (!QFile::rename(override_path, mcPath)) { emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); return; } } + // Do client overrides + auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(client_override_path)) { + if (!FS::overrideFolder(mcPath, client_override_path)) { + emitFailed(tr("Could not rename the client overrides folder:\n") + "client overrides"); + return; + } + } + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared<INISettingsObject>(configPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); @@ -735,13 +740,24 @@ void InstanceImportTask::processModrinth() instance.saveNow(); m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for (auto &file : files) + for (auto file : files) { auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); - qDebug() << "Will download" << file.download << "to" << path; - auto dl = Net::Download::makeFile(file.download, path); + qDebug() << "Will try to download" << file.downloads.front() << "to" << path; + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_filesNetJob->addNetAction(dl); + + if (file.downloads.size() > 0) { + // FIXME: This really needs to be put into a ConcurrentTask of + // MultipleOptionsTask's , once those exist :) + connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{ + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + dl->succeeded(); + }); + } } connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 847d897e..3e3c81f7 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -547,8 +547,20 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) auto instanceRoot = FS::PathCombine(m_instDir, id); auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; - // TODO: Handle incompatible instances - inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + + instanceSettings->registerSetting("InstanceType", ""); + + QString inst_type = instanceSettings->get("InstanceType").toString(); + + // NOTE: Some PolyMC versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a OneSix instance + if (inst_type == "OneSix" || inst_type.isEmpty()) + { + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + } qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); return inst; } diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 002c08b9..d36ee3fe 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -105,6 +105,11 @@ void LaunchController::decideAccount() // Open the account manager. APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); } + else if (reply == QMessageBox::No) + { + // Do not open "profile select" dialog. + return; + } } m_accountToUse = accounts->defaultAccount(); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 8591fcc0..627ceaf1 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -151,23 +151,23 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const continue; if (mod.type() == Mod::MOD_ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles)) + if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles)) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } } else if (mod.type() == Mod::MOD_SINGLEFILE) { // FIXME: buggy - does not work with addedFiles - auto filename = mod.filename(); + auto filename = mod.fileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); @@ -176,7 +176,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles - auto filename = mod.filename(); + auto filename = mod.fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); @@ -193,7 +193,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder " << filename.fileName() << " from " @@ -204,7 +204,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add unknown mod type" << mod.fileinfo().fileName() << "to the jar."; return false; } } diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 08a02d29..41856fb5 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -1,24 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + #include "ModDownloadTask.h" + #include "Application.h" +#include "minecraft/mod/ModFolderModel.h" -ModDownloadTask::ModDownloadTask(const QUrl sourceUrl,const QString filename, const std::shared_ptr<ModFolderModel> mods) -: m_sourceUrl(sourceUrl), mods(mods), filename(filename) { -} +ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed) + : m_mod(mod), m_mod_version(version), mods(mods) +{ + if (is_indexed) { + m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); -void ModDownloadTask::executeTask() { - setStatus(tr("Downloading mod:\n%1").arg(m_sourceUrl.toString())); + addTask(m_update_task); + } m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::Download::makeFile(m_sourceUrl, mods->dir().absoluteFilePath(filename))); + m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl)); + + m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename()))); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed); - m_filesNetJob->start(); + + addTask(m_filesNetJob); + } void ModDownloadTask::downloadSucceeded() { - emitSucceeded(); m_filesNetJob.reset(); } @@ -32,8 +58,3 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } - -bool ModDownloadTask::abort() { - return m_filesNetJob->abort(); -} - diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index ddada5a2..b3c25909 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -1,28 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + #pragma once -#include "QObjectPtr.h" -#include "tasks/Task.h" -#include "minecraft/mod/ModFolderModel.h" + #include "net/NetJob.h" -#include <QUrl> +#include "tasks/SequentialTask.h" +#include "modplatform/ModIndex.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" -class ModDownloadTask : public Task { +class ModFolderModel; + +class ModDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr<ModFolderModel> mods); - const QString& getFilename() const { return filename; } - -public slots: - bool abort() override; -protected: - //! Entry point for tasks. - void executeTask() override; + explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed); + const QString& getFilename() const { return m_mod_version.fileName; } private: - QUrl m_sourceUrl; - NetJob::Ptr m_filesNetJob; + ModPlatform::IndexedPack m_mod; + ModPlatform::IndexedVersion m_mod_version; const std::shared_ptr<ModFolderModel> mods; - const QString filename; + + NetJob::Ptr m_filesNetJob; + LocalModUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 0ddfae55..d426aa80 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -56,6 +56,15 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren emit iconUpdated({}); } +void IconList::sortIconList() +{ + qDebug() << "Sorting icon list..."; + std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { + return a.m_key.localeAwareCompare(b.m_key) < 0; + }); + reindex(); +} + void IconList::directoryChanged(const QString &path) { QDir new_dir (path); @@ -141,6 +150,8 @@ void IconList::directoryChanged(const QString &path) emit iconUpdated(key); } } + + sortIconList(); } void IconList::fileChanged(const QString &path) diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index ebbb52e2..f9f49e7f 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -71,6 +71,7 @@ private: // hide assign op IconList &operator=(const IconList &) = delete; void reindex(); + void sortIconList(); public slots: void directoryChanged(const QString &path); diff --git a/launcher/main.cpp b/launcher/main.cpp index 85c5fdee..3d25b4ff 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -27,10 +27,6 @@ int main(int argc, char *argv[]) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - // initialize Qt Application app(argc, argv); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 61326fac..7e72601f 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -168,6 +168,8 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + m_settings->set("InstanceType", "OneSix"); + m_components.reset(new PackProfile(this)); } @@ -659,23 +661,23 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << QString("%1:").arg(label); auto modList = model.allMods(); std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { - auto aName = a.filename().completeBaseName(); - auto bName = b.filename().completeBaseName(); + auto aName = a.fileinfo().completeBaseName(); + auto bName = b.fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); for(auto & mod: modList) { if(mod.type() == Mod::MOD_FOLDER) { - out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)"; + out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)"; continue; } if(mod.enabled()) { - out << u8" [✔️] " + mod.filename().completeBaseName(); + out << u8" [✔️] " + mod.fileinfo().completeBaseName(); } else { - out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)"; + out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)"; } } @@ -1013,7 +1015,8 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new ModFolderModel(modsRoot())); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed)); m_loader_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } @@ -1024,7 +1027,8 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new ModFolderModel(coreModsDir())); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed)); m_core_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d7010355..427bc32b 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -1,21 +1,42 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "LauncherPartLaunch.h" #include <QStandardPaths> +#include <QRegularExpression> #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h new file mode 100644 index 00000000..56962818 --- /dev/null +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <memory> + +#include "modplatform/packwiz/Packwiz.h" + +// launcher/minecraft/mod/Mod.h +class Mod; + +/* Abstraction file for easily changing the way metadata is stored / handled + * Needs to be a class because of -Wunused-function and no C++17 [[maybe_unused]] + * */ +class Metadata { + public: + using ModStruct = Packwiz::V1::Mod; + + static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct + { + return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); + } + + static auto create(QDir& index_dir, Mod& internal_mod) -> ModStruct + { + return Packwiz::V1::createModFormat(index_dir, internal_mod); + } + + static void update(QDir& index_dir, ModStruct& mod) + { + Packwiz::V1::updateModIndex(index_dir, mod); + } + + static void remove(QDir& index_dir, QString& mod_name) + { + Packwiz::V1::deleteModIndex(index_dir, mod_name); + } + + static auto get(QDir& index_dir, QString& mod_name) -> ModStruct + { + return Packwiz::V1::getIndexForMod(index_dir, mod_name); + } +}; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index b6bff29b..742709e3 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -1,24 +1,49 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "Mod.h" #include <QDir> #include <QString> -#include "Mod.h" -#include <QDebug> #include <FileSystem.h> +#include <QDebug> + +#include "Application.h" +#include "MetadataHandler.h" namespace { @@ -26,57 +51,67 @@ ModDetails invalidDetails; } - -Mod::Mod(const QFileInfo &file) +Mod::Mod(const QFileInfo& file) { repath(file); m_changedDateTime = file.lastModified(); } -void Mod::repath(const QFileInfo &file) +Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) + : m_file(mods_dir.absoluteFilePath(metadata.filename)) + , m_internal_id(metadata.filename) + , m_name(metadata.name) +{ + if (m_file.isDir()) { + m_type = MOD_FOLDER; + } else { + if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar")) + m_type = MOD_ZIPFILE; + else if (metadata.filename.endsWith(".litemod")) + m_type = MOD_LITEMOD; + else + m_type = MOD_SINGLEFILE; + } + + m_enabled = true; + m_changedDateTime = m_file.lastModified(); + + m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata)); +} + +void Mod::repath(const QFileInfo& file) { m_file = file; QString name_base = file.fileName(); m_type = Mod::MOD_UNKNOWN; - m_mmc_id = name_base; + m_internal_id = name_base; - if (m_file.isDir()) - { + if (m_file.isDir()) { m_type = MOD_FOLDER; m_name = name_base; - } - else if (m_file.isFile()) - { - if (name_base.endsWith(".disabled")) - { + } else if (m_file.isFile()) { + if (name_base.endsWith(".disabled")) { m_enabled = false; name_base.chop(9); - } - else - { + } else { m_enabled = true; } - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) - { + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) { m_type = MOD_ZIPFILE; name_base.chop(4); - } - else if (name_base.endsWith(".litemod")) - { + } else if (name_base.endsWith(".litemod")) { m_type = MOD_LITEMOD; name_base.chop(8); - } - else - { + } else { m_type = MOD_SINGLEFILE; } m_name = name_base; } } -bool Mod::enable(bool value) +auto Mod::enable(bool value) -> bool { if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) return false; @@ -85,67 +120,126 @@ bool Mod::enable(bool value) return false; QString path = m_file.absoluteFilePath(); - if (value) - { - QFile foo(path); + QFile file(path); + if (value) { if (!path.endsWith(".disabled")) return false; path.chop(9); - if (!foo.rename(path)) + + if (!file.rename(path)) return false; - } - else - { - QFile foo(path); + } else { path += ".disabled"; - if (!foo.rename(path)) + + if (!file.rename(path)) return false; } - repath(QFileInfo(path)); + + if (status() == ModStatus::NoMetadata) + repath(QFileInfo(path)); + m_enabled = value; return true; } -bool Mod::destroy() +void Mod::setStatus(ModStatus status) { - m_type = MOD_UNKNOWN; - return FS::deletePath(m_file.filePath()); + if (m_localDetails) { + m_localDetails->status = status; + } else { + m_temp_status = status; + } } +void Mod::setMetadata(Metadata::ModStruct* metadata) +{ + if (status() == ModStatus::NoMetadata) + setStatus(ModStatus::Installed); + if (m_localDetails) { + m_localDetails->metadata.reset(metadata); + } else { + m_temp_metadata.reset(metadata); + } +} -const ModDetails & Mod::details() const +auto Mod::destroy(QDir& index_dir) -> bool { - if(!m_localDetails) - return invalidDetails; - return *m_localDetails; -} + auto n = name(); + // FIXME: This can fail to remove the metadata if the + // "ModMetadataDisabled" setting is on, since there could + // be a name mismatch! + Metadata::remove(index_dir, n); + m_type = MOD_UNKNOWN; + return FS::deletePath(m_file.filePath()); +} -QString Mod::version() const +auto Mod::details() const -> const ModDetails& { - return details().version; + return m_localDetails ? *m_localDetails : invalidDetails; } -QString Mod::name() const +auto Mod::name() const -> QString { - auto & d = details(); - if(!d.name.isEmpty()) { - return d.name; + auto d_name = details().name; + if (!d_name.isEmpty()) { + return d_name; } return m_name; } -QString Mod::homeurl() const +auto Mod::version() const -> QString +{ + return details().version; +} + +auto Mod::homeurl() const -> QString { return details().homeurl; } -QString Mod::description() const +auto Mod::description() const -> QString { return details().description; } -QStringList Mod::authors() const +auto Mod::authors() const -> QStringList { return details().authors; } + +auto Mod::status() const -> ModStatus +{ + if (!m_localDetails) + return m_temp_status; + return details().status; +} + +auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct> +{ + if (m_localDetails) + return m_localDetails->metadata; + return m_temp_metadata; +} + +auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct> +{ + if (m_localDetails) + return m_localDetails->metadata; + return m_temp_metadata; +} + +void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details) +{ + m_resolving = false; + m_resolved = true; + m_localDetails = details; + + if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { + m_localDetails->metadata = m_temp_metadata; + if (status() == ModStatus::NoMetadata) + setStatus(ModStatus::Installed); + } + + setStatus(m_temp_status); +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 921faeb1..5f9c4684 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -1,28 +1,46 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #pragma once -#include <QFileInfo> + #include <QDateTime> +#include <QFileInfo> #include <QList> -#include <memory> #include "ModDetails.h" - - class Mod { public: @@ -32,84 +50,73 @@ public: MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). MOD_FOLDER, //!< The mod is in a folder on the filesystem. - MOD_LITEMOD, //!< The mod is a litemod + MOD_LITEMOD, //!< The mod is a litemod }; Mod() = default; Mod(const QFileInfo &file); + explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); - QFileInfo filename() const - { - return m_file; - } - QString mmc_id() const - { - return m_mmc_id; - } - ModType type() const - { - return m_type; - } - bool valid() - { - return m_type != MOD_UNKNOWN; - } + auto fileinfo() const -> QFileInfo { return m_file; } + auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; } + auto internal_id() const -> QString { return m_internal_id; } + auto type() const -> ModType { return m_type; } + auto enabled() const -> bool { return m_enabled; } - QDateTime dateTimeChanged() const - { - return m_changedDateTime; - } + auto valid() const -> bool { return m_type != MOD_UNKNOWN; } - bool enabled() const - { - return m_enabled; - } + auto details() const -> const ModDetails&; + auto name() const -> QString; + auto version() const -> QString; + auto homeurl() const -> QString; + auto description() const -> QString; + auto authors() const -> QStringList; + auto status() const -> ModStatus; - const ModDetails &details() const; + auto metadata() -> std::shared_ptr<Metadata::ModStruct>; + auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>; - QString name() const; - QString version() const; - QString homeurl() const; - QString description() const; - QStringList authors() const; + void setStatus(ModStatus status); + void setMetadata(Metadata::ModStruct* metadata); - bool enable(bool value); + auto enable(bool value) -> bool; // delete all the files of this mod - bool destroy(); + auto destroy(QDir& index_dir) -> bool; // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); - bool shouldResolve() { - return !m_resolving && !m_resolved; - } - bool isResolving() { - return m_resolving; - } - int resolutionTicket() - { - return m_resolutionTicket; - } + auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; } + auto isResolving() const -> bool { return m_resolving; } + auto resolutionTicket() const -> int { return m_resolutionTicket; } + void setResolving(bool resolving, int resolutionTicket) { m_resolving = resolving; m_resolutionTicket = resolutionTicket; } - void finishResolvingWithDetails(std::shared_ptr<ModDetails> details){ - m_resolving = false; - m_resolved = true; - m_localDetails = details; - } + void finishResolvingWithDetails(std::shared_ptr<ModDetails> details); protected: QFileInfo m_file; QDateTime m_changedDateTime; - QString m_mmc_id; + + QString m_internal_id; + /* Name as reported via the file name */ QString m_name; + ModType m_type = MOD_UNKNOWN; + + /* If the mod has metadata, this will be filled in the constructor, and passed to + * the ModDetails when calling finishResolvingWithDetails */ + std::shared_ptr<Metadata::ModStruct> m_temp_metadata; + + /* Set the mod status while it doesn't have local details just yet */ + ModStatus m_temp_status = ModStatus::NotInstalled; + + std::shared_ptr<ModDetails> m_localDetails; + bool m_enabled = true; bool m_resolving = false; bool m_resolved = false; int m_resolutionTicket = 0; - ModType m_type = MOD_UNKNOWN; - std::shared_ptr<ModDetails> m_localDetails; }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 6ab4aee7..3e0a7ab0 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -1,17 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + #pragma once +#include <memory> + #include <QString> #include <QStringList> +#include "minecraft/mod/MetadataHandler.h" + +enum class ModStatus { + Installed, // Both JAR and Metadata are present + NotInstalled, // Only the Metadata is present + NoMetadata, // Only the JAR is present +}; + struct ModDetails { + /* Mod ID as defined in the ModLoader-specific metadata */ QString mod_id; + + /* Human-readable name */ QString name; + + /* Human-readable mod version */ QString version; + + /* Human-readable minecraft version */ QString mcversion; + + /* URL for mod's home page */ QString homeurl; - QString updateurl; + + /* Human-readable description */ QString description; + + /* List of the author's names */ QStringList authors; - QString credits; + + /* Installation status of the mod */ + ModStatus status; + + /* Metadata information, if any */ + std::shared_ptr<Metadata::ModStruct> metadata; }; diff --git a/launcher/minecraft/mod/ModFolderLoadTask.cpp b/launcher/minecraft/mod/ModFolderLoadTask.cpp deleted file mode 100644 index 88349877..00000000 --- a/launcher/minecraft/mod/ModFolderLoadTask.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "ModFolderLoadTask.h" -#include <QDebug> - -ModFolderLoadTask::ModFolderLoadTask(QDir dir) : - m_dir(dir), m_result(new Result()) -{ -} - -void ModFolderLoadTask::run() -{ - m_dir.refresh(); - for (auto entry : m_dir.entryInfoList()) - { - Mod m(entry); - m_result->mods[m.mmc_id()] = m; - } - emit succeeded(); -} diff --git a/launcher/minecraft/mod/ModFolderLoadTask.h b/launcher/minecraft/mod/ModFolderLoadTask.h deleted file mode 100644 index 8d720e65..00000000 --- a/launcher/minecraft/mod/ModFolderLoadTask.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include <QRunnable> -#include <QObject> -#include <QDir> -#include <QMap> -#include "Mod.h" -#include <memory> - -class ModFolderLoadTask : public QObject, public QRunnable -{ - Q_OBJECT -public: - struct Result { - QMap<QString, Mod> mods; - }; - using ResultPtr = std::shared_ptr<Result>; - ResultPtr result() const { - return m_result; - } - -public: - ModFolderLoadTask(QDir dir); - void run(); -signals: - void succeeded(); -private: - QDir m_dir; - ResultPtr m_result; -}; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index f0c53c39..ded2d3a2 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -1,32 +1,55 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #include "ModFolderModel.h" + #include <FileSystem.h> +#include <QDebug> +#include <QFileSystemWatcher> #include <QMimeData> -#include <QUrl> -#include <QUuid> #include <QString> -#include <QFileSystemWatcher> -#include <QDebug> -#include "ModFolderLoadTask.h" #include <QThreadPool> +#include <QUrl> +#include <QUuid> #include <algorithm> -#include "LocalModParseTask.h" -ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) +#include "minecraft/mod/tasks/LocalModParseTask.h" +#include "minecraft/mod/tasks/ModFolderLoadTask.h" + +ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); @@ -79,10 +102,14 @@ bool ModFolderModel::update() return true; } - auto task = new ModFolderLoadTask(m_dir); + auto index_dir = indexDir(); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); + threadPool->start(task); return true; } @@ -153,7 +180,7 @@ void ModFolderModel::finishUpdate() modsIndex.clear(); int idx = 0; for(auto & mod: mods) { - modsIndex[mod.mmc_id()] = idx; + modsIndex[mod.internal_id()] = idx; idx++; } } @@ -174,9 +201,9 @@ void ModFolderModel::resolveMod(Mod& m) return; } - auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo()); auto result = task->result(); - result->id = m.mmc_id(); + result->id = m.internal_id(); activeTickets.insert(nextResolutionTicket, result); m.setResolving(true, nextResolutionTicket); nextResolutionTicket++; @@ -333,8 +360,12 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) for (auto i: indexes) { + if(i.column() != 0) { + continue; + } Mod &m = mods[i.row()]; - m.destroy(); + auto index_dir = indexDir(); + m.destroy(index_dir); } return true; } @@ -381,7 +412,7 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: - return mods[row].mmc_id(); + return mods[row].internal_id(); case Qt::CheckStateRole: switch (column) @@ -436,11 +467,11 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio } // preserve the row, but change its ID - auto oldId = mod.mmc_id(); + auto oldId = mod.internal_id(); if(!mod.enable(!mod.enabled())) { return false; } - auto newId = mod.mmc_id(); + auto newId = mod.internal_id(); if(modsIndex.contains(newId)) { // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled // But is it necessary? diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 62c504df..24b4d358 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -1,17 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #pragma once @@ -24,8 +45,8 @@ #include "Mod.h" -#include "ModFolderLoadTask.h" -#include "LocalModParseTask.h" +#include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalModParseTask.h" class LegacyInstance; class BaseInstance; @@ -52,7 +73,7 @@ public: Enable, Toggle }; - ModFolderModel(const QString &dir); + ModFolderModel(const QString &dir, bool is_indexed = false); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; @@ -108,11 +129,16 @@ public: bool isValid(); - QDir dir() + QDir& dir() { return m_dir; } + QDir indexDir() + { + return { QString("%1/.index").arg(dir().absolutePath()) }; + } + const QList<Mod> & allMods() { return mods; @@ -141,6 +167,7 @@ protected: bool scheduled_update = false; bool interaction_disabled = false; QDir m_dir; + bool m_is_indexed; QMap<QString, int> modsIndex; QMap<int, LocalModParseTask::ResultPtr> activeTickets; int nextResolutionTicket = 0; diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp index 76f16ed5..34a3b83a 100644 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ b/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #include <QTest> #include <QTemporaryDir> @@ -32,8 +66,11 @@ slots: { QString folder = source; QTemporaryDir tempDir; - ModFolderModel m(tempDir.path()); + QEventLoop loop; + ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); m.installMod(folder); + loop.exec(); verify(tempDir.path()); } @@ -41,8 +78,11 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; - ModFolderModel m(tempDir.path()); + QEventLoop loop; + ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); m.installMod(folder); + loop.exec(); verify(tempDir.path()); } } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index f3d7f566..276804ed 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + #include "ResourcePackFolderModel.h" ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index d5956da1..e3a22219 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + #include "TexturePackFolderModel.h" TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index a7bec5ae..3354732b 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -35,7 +35,6 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) details->name = name; } details->version = firstObj.value("version").toString(); - details->updateurl = firstObj.value("updateUrl").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); if(!homeurl.isEmpty()) { @@ -57,7 +56,6 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) { details->authors.append(author.toString()); } - details->credits = firstObj.value("credits").toString(); return details; }; QJsonParseError jsonError; @@ -168,27 +166,9 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents) } if(!authors.isEmpty()) { - // author information is stored as a string now, not a list details->authors.append(authors); } - // is credits even used anywhere? including this for completion/parity with old data version - toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); - QString credits = ""; - if(creditsDatum.ok) - { - authors = creditsDatum.u.s; - free(creditsDatum.u.s); - } - else - { - creditsDatum = toml_string_in(tomlModsTable0, "credits"); - if(creditsDatum.ok) - { - credits = creditsDatum.u.s; - free(creditsDatum.u.s); - } - } - details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); QString homeurl = ""; if(homeurlDatum.ok) diff --git a/launcher/minecraft/mod/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 0f119ba6..ed92394c 100644 --- a/launcher/minecraft/mod/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -1,9 +1,11 @@ #pragma once -#include <QRunnable> + #include <QDebug> #include <QObject> -#include "Mod.h" -#include "ModDetails.h" +#include <QRunnable> + +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ModDetails.h" class LocalModParseTask : public QObject, public QRunnable { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp new file mode 100644 index 00000000..1bdecb8c --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "LocalModUpdateTask.h" + +#include "Application.h" +#include "FileSystem.h" +#include "minecraft/mod/MetadataHandler.h" + +#ifdef Q_OS_WIN32 +#include <windows.h> +#endif + +LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version) + : m_index_dir(index_dir), m_mod(mod), m_mod_version(mod_version) +{ + // Ensure a '.index' folder exists in the mods folder, and create it if it does not + if (!FS::ensureFolderPathExists(index_dir.path())) { + emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); + } + +#ifdef Q_OS_WIN32 + SetFileAttributesA(index_dir.path().toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif +} + +void LocalModUpdateTask::executeTask() +{ + setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + + auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); + Metadata::update(m_index_dir, pw_mod); + + emitSucceeded(); +} + +auto LocalModUpdateTask::abort() -> bool +{ + emitAborted(); + return true; +} diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h new file mode 100644 index 00000000..2db183e0 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <QDir> + +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class LocalModUpdateTask : public Task { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<LocalModUpdateTask>; + + explicit LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); + + auto canAbort() const -> bool override { return true; } + auto abort() -> bool override; + + protected slots: + //! Entry point for tasks. + void executeTask() override; + + private: + QDir m_index_dir; + ModPlatform::IndexedPack& m_mod; + ModPlatform::IndexedVersion& m_mod_version; +}; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp new file mode 100644 index 00000000..276414e4 --- /dev/null +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include "ModFolderLoadTask.h" + +#include "Application.h" +#include "minecraft/mod/MetadataHandler.h" + +ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed) + : m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_result(new Result()) +{} + +void ModFolderLoadTask::run() +{ + if (m_is_indexed) { + // Read metadata first + getFromMetadata(); + } + + // Read JAR files that don't have metadata + m_mods_dir.refresh(); + for (auto entry : m_mods_dir.entryInfoList()) { + Mod mod(entry); + + if (mod.enabled()) { + if (m_result->mods.contains(mod.internal_id())) { + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + } + else { + m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } + } + else { + QString chopped_id = mod.internal_id().chopped(9); + if (m_result->mods.contains(chopped_id)) { + m_result->mods[mod.internal_id()] = mod; + + auto metadata = m_result->mods[chopped_id].metadata(); + if (metadata) { + mod.setMetadata(new Metadata::ModStruct(*metadata)); + + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + m_result->mods.remove(chopped_id); + } + } + else { + m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } + } + } + + emit succeeded(); +} + +void ModFolderLoadTask::getFromMetadata() +{ + m_index_dir.refresh(); + for (auto entry : m_index_dir.entryList(QDir::Files)) { + auto metadata = Metadata::get(m_index_dir, entry); + + if(!metadata.isValid()){ + return; + } + + Mod mod(m_mods_dir, metadata); + mod.setStatus(ModStatus::NotInstalled); + m_result->mods[mod.internal_id()] = mod; + } +} diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h new file mode 100644 index 00000000..088f873e --- /dev/null +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 2013-2021 MultiMC Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#pragma once + +#include <QDir> +#include <QMap> +#include <QObject> +#include <QRunnable> +#include <memory> +#include "minecraft/mod/Mod.h" + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QMap<QString, Mod> mods; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + +public: + ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed); + void run(); +signals: + void succeeded(); + +private: + void getFromMetadata(); + +private: + QDir& m_mods_dir, m_index_dir; + bool m_is_indexed; + ResultPtr m_result; +}; diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index eb0de3f0..91b760df 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -7,6 +7,7 @@ namespace ModPlatform { class ListModel; +struct IndexedPack; } class ModAPI { @@ -35,6 +36,7 @@ class ModAPI { }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; + virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; struct VersionSearchArgs { diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp new file mode 100644 index 00000000..3c4b7887 --- /dev/null +++ b/launcher/modplatform/ModIndex.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "modplatform/ModIndex.h" + +#include <QCryptographicHash> + +namespace ModPlatform { + +auto ProviderCapabilities::name(Provider p) -> const char* +{ + switch (p) { + case Provider::MODRINTH: + return "modrinth"; + case Provider::FLAME: + return "curseforge"; + } + return {}; +} +auto ProviderCapabilities::readableName(Provider p) -> QString +{ + switch (p) { + case Provider::MODRINTH: + return "Modrinth"; + case Provider::FLAME: + return "CurseForge"; + } + return {}; +} +auto ProviderCapabilities::hashType(Provider p) -> QStringList +{ + switch (p) { + case Provider::MODRINTH: + return { "sha512", "sha1" }; + case Provider::FLAME: + // Try newer formats first, fall back to old format + return { "sha1", "md5", "murmur2" }; + } + return {}; +} +auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray +{ + switch (p) { + case Provider::MODRINTH: { + // NOTE: Data is the result of reading the entire JAR file! + + // If 'type' was specified, we use that + if (!type.isEmpty() && hashType(p).contains(type)) { + if (type == "sha512") + return QCryptographicHash::hash(data, QCryptographicHash::Sha512); + else if (type == "sha1") + return QCryptographicHash::hash(data, QCryptographicHash::Sha1); + } + + return QCryptographicHash::hash(data, QCryptographicHash::Sha512); + } + case Provider::FLAME: + // If 'type' was specified, we use that + if (!type.isEmpty() && hashType(p).contains(type)) { + if(type == "sha1") + return QCryptographicHash::hash(data, QCryptographicHash::Sha1); + else if (type == "md5") + return QCryptographicHash::hash(data, QCryptographicHash::Md5); + } + + break; + } + return {}; +} + +} // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7e1cf254..c27643af 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + #pragma once #include <QList> @@ -8,11 +26,30 @@ namespace ModPlatform { +enum class Provider { + MODRINTH, + FLAME +}; + +class ProviderCapabilities { + public: + auto name(Provider) -> const char*; + auto readableName(Provider) -> QString; + auto hashType(Provider) -> QStringList; + auto hash(Provider, QByteArray&, QString type = "") -> QByteArray; +}; + struct ModpackAuthor { QString name; QString url; }; +struct DonationData { + QString id; + QString platform; + QString url; +}; + struct IndexedVersion { QVariant addonId; QVariant fileId; @@ -22,10 +59,22 @@ struct IndexedVersion { QString date; QString fileName; QVector<QString> loaders = {}; + QString hash_type; + QString hash; +}; + +struct ExtraPackData { + QList<DonationData> donate; + + QString issuesUrl; + QString sourceUrl; + QString wikiUrl; + QString discordUrl; }; struct IndexedPack { QVariant addonId; + Provider provider; QString name; QString description; QList<ModpackAuthor> authors; @@ -35,8 +84,13 @@ struct IndexedPack { bool versionsLoaded = false; QVector<IndexedVersion> versions; + + // Don't load by default, since some modplatform don't have that info + bool extraDataLoaded = true; + ExtraPackData extraData; }; } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) +Q_DECLARE_METATYPE(ModPlatform::Provider) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 62c7bf6d..b4936bd8 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -60,10 +60,11 @@ namespace ATLauncher { static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version); -PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version) { m_support = support; - m_pack = pack; + m_pack_name = packName; + m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); m_version_name = version; } @@ -81,7 +82,7 @@ void PackInstallTask::executeTask() qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network()); auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") - .arg(m_pack).arg(m_version_name); + .arg(m_pack_safe_name).arg(m_version_name); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -98,7 +99,7 @@ void PackInstallTask::onDownloadSucceeded() QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -319,7 +320,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto f = std::make_shared<VersionFile>(); - f->name = m_pack + " " + m_version_name + " (libraries)"; + f->name = m_pack_name + " " + m_version_name + " (libraries)"; const static QMap<QString, QString> liteLoaderMap = { { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, @@ -465,7 +466,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< } auto f = std::make_shared<VersionFile>(); - f->name = m_pack + " " + m_version_name; + f->name = m_pack_name + " " + m_version_name; if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { f->mainClass = mainClass; } @@ -507,9 +508,9 @@ void PackInstallTask::installConfigs() setStatus(tr("Downloading configs...")); jobPtr = new NetJob(tr("Config download"), APPLICATION->network()); - auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); + auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") - .arg(m_pack).arg(m_version_name); + .arg(m_pack_safe_name).arg(m_version_name); auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); @@ -862,6 +863,7 @@ void PackInstallTask::install() instance.setName(m_instName); instance.setIconKey(m_instIcon); + instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); instanceSettings->resumeSave(); jarmods.clear(); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index f0af4e3a..f55873e9 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -75,7 +75,7 @@ class PackInstallTask : public InstanceTask Q_OBJECT public: - explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version); + explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version); virtual ~PackInstallTask(){} bool canAbort() const override { return true; } @@ -117,7 +117,8 @@ private: NetJob::Ptr jobPtr; QByteArray response; - QString m_pack; + QString m_pack_name; + QString m_pack_safe_name; QString m_version_name; PackVersion m_version; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8bb33d47..424153d2 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,5 +1,6 @@ #pragma once +#include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" class FlameAPI : public NetworkModAPI { @@ -41,6 +42,11 @@ class FlameAPI : public NetworkModAPI { .arg(gameVersionStr); }; + inline auto getModInfoURL(QString& id) const -> QString override + { + return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ba0824cf..b99bfb26 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -6,9 +6,12 @@ #include "modplatform/flame/FlameAPI.h" #include "net/NetJob.h" +static ModPlatform::ProviderCapabilities ProviderCaps; + void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); + pack.provider = ModPlatform::Provider::FLAME; pack.name = Json::requireString(obj, "name"); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); @@ -25,6 +28,38 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } + + loadExtraPackData(pack, obj); +} + +void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + auto links_obj = Json::ensureObject(obj, "links"); + + pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + if(pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + if(pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + if(pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraDataLoaded = true; +} + +static QString enumToString(int hash_algorithm) +{ + switch(hash_algorithm){ + default: + case 1: + return "sha1"; + case 2: + return "md5"; + } } void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, @@ -38,28 +73,13 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj); + if(!file.addonId.isValid()) + file.addonId = pack.addonId; - auto versionArray = Json::requireArray(obj, "gameVersions"); - if (versionArray.isEmpty()) { - continue; - } - - ModPlatform::IndexedVersion file; - for (auto mcVer : versionArray) { - auto str = mcVer.toString(); - - if (str.contains('.')) - file.mcVersion.append(str); - } - - file.addonId = pack.addonId; - file.fileId = Json::requireInteger(obj, "id"); - file.date = Json::requireString(obj, "fileDate"); - file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); - file.fileName = Json::requireString(obj, "fileName"); - - unsortedVersions.append(file); + if(file.fileId.isValid()) // Heuristic to check if the returned value is valid + unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -70,3 +90,39 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion +{ + auto versionArray = Json::requireArray(obj, "gameVersions"); + if (versionArray.isEmpty()) { + return {}; + } + + ModPlatform::IndexedVersion file; + for (auto mcVer : versionArray) { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + } + + file.addonId = Json::requireInteger(obj, "modId"); + file.fileId = Json::requireInteger(obj, "id"); + file.date = Json::requireString(obj, "fileDate"); + file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::requireString(obj, "downloadUrl"); + file.fileName = Json::requireString(obj, "fileName"); + + auto hash_list = Json::ensureArray(obj, "hashes"); + for (auto h : hash_list) { + auto hash_entry = Json::ensureObject(h); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); + auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); + if (hash_types.contains(hash_algo)) { + file.hash = Json::requireString(hash_entry, "value"); + file.hash_type = hash_algo; + break; + } + } + return file; +} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index d3171d94..9c6c1c6c 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -12,9 +12,11 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance* inst); +auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index bece7843..ad48b7b6 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -6,7 +6,6 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); auto logo = Json::requireObject(obj, "logo"); @@ -46,6 +45,32 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) if (!found) { throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); } + + loadIndexedInfo(pack, obj); +} + +void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj) +{ + auto links_obj = Json::ensureObject(obj, "links"); + + pack.extra.websiteUrl = Json::ensureString(links_obj, "websiteUrl"); + if(pack.extra.websiteUrl.endsWith('/')) + pack.extra.websiteUrl.chop(1); + + pack.extra.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + if(pack.extra.issuesUrl.endsWith('/')) + pack.extra.issuesUrl.chop(1); + + pack.extra.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + if(pack.extra.sourceUrl.endsWith('/')) + pack.extra.sourceUrl.chop(1); + + pack.extra.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + if(pack.extra.wikiUrl.endsWith('/')) + pack.extra.wikiUrl.chop(1); + + pack.extraInfoLoaded = true; + } void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 7ffa29c3..1ca0fc0e 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -20,6 +20,13 @@ struct IndexedVersion { QString downloadUrl; }; +struct ModpackExtra { + QString websiteUrl; + QString wikiUrl; + QString issuesUrl; + QString sourceUrl; +}; + struct IndexedPack { int addonId; @@ -28,13 +35,16 @@ struct IndexedPack QList<ModpackAuthor> authors; QString logoName; QString logoUrl; - QString websiteUrl; bool versionsLoaded = false; QVector<IndexedVersion> versions; + + bool extraInfoLoaded = false; + ModpackExtra extra; }; void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedInfo(IndexedPack&, QJsonObject&); void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); } diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 6829b837..d7abd10f 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -31,6 +31,31 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const netJob->start(); } +void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) +{ + auto id_str = pack.addonId.toString(); + auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network()); + auto searchUrl = getModInfoURL(id_str); + + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->infoRequestFinished(doc, pack); + }); + + netJob->start(); +} + void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const { auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network()); diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 000620b2..87d77ad1 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -5,9 +5,11 @@ class NetworkModAPI : public ModAPI { public: void searchMods(CallerType* caller, SearchArgs&& args) const override; + void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; + virtual auto getModInfoURL(QString& id) const -> QString = 0; virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0; }; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 33df6fa4..c324ffda 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FTBPackInstallTask.h" @@ -80,7 +99,7 @@ void PackInstallTask::onDownloadSucceeded() QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -220,6 +239,7 @@ void PackInstallTask::install() instance.setName(m_instName); instance.setIconKey(m_instIcon); + instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); instanceSettings->resumeSave(); emitSucceeded(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 6119a4df..89e52d6c 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -20,6 +20,7 @@ #include "BuildConfig.h" #include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" #include <QDebug> @@ -75,6 +76,11 @@ class ModrinthAPI : public NetworkModAPI { .arg(getGameVersionsArray(args.versions)); }; + inline auto getModInfoURL(QString& id) const -> QString override + { + return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { return QString(BuildConfig.MODRINTH_PROD_URL + diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index f7fa9864..b6f5490a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,19 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ #include "ModrinthPackIndex.h" #include "ModrinthAPI.h" @@ -24,10 +25,12 @@ #include "net/NetJob.h" static ModrinthAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); + pack.provider = ModPlatform::Provider::MODRINTH; pack.name = Json::requireString(obj, "title"); QString slug = Json::ensureString(obj, "slug", ""); @@ -45,6 +48,43 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) modAuthor.name = Json::requireString(obj, "author"); modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); + + // Modrinth can have more data than what's provided by the basic search :) + pack.extraDataLoaded = false; +} + +void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url"); + if(pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(obj, "source_url"); + if(pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url"); + if(pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraData.discordUrl = Json::ensureString(obj, "discord_url"); + if(pack.extraData.discordUrl.endsWith('/')) + pack.extraData.discordUrl.chop(1); + + auto donate_arr = Json::ensureArray(obj, "donation_urls"); + for(auto d : donate_arr){ + auto d_obj = Json::requireObject(d); + + ModPlatform::DonationData donate; + + donate.id = Json::ensureString(d_obj, "id"); + donate.platform = Json::ensureString(d_obj, "platform"); + donate.url = Json::ensureString(d_obj, "url"); + + pack.extraData.donate.append(donate); + } + + pack.extraDataLoaded = true; } void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, @@ -57,46 +97,10 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); - ModPlatform::IndexedVersion file; - file.addonId = Json::requireString(obj, "project_id"); - file.fileId = Json::requireString(obj, "id"); - file.date = Json::requireString(obj, "date_published"); - auto versionArray = Json::requireArray(obj, "game_versions"); - if (versionArray.empty()) { continue; } - for (auto mcVer : versionArray) { - file.mcVersion.append(mcVer.toString()); - } - auto loaders = Json::requireArray(obj, "loaders"); - for (auto loader : loaders) { - file.loaders.append(loader.toString()); - } - file.version = Json::requireString(obj, "name"); - - auto files = Json::requireArray(obj, "files"); - int i = 0; - - // Find correct file (needed in cases where one version may have multiple files) - // Will default to the last one if there's no primary (though I think Modrinth requires that - // at least one file is primary, idk) - // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed - while (i < files.count() - 1){ - auto parent = files[i].toObject(); - auto fileName = Json::requireString(parent, "filename"); - - // Grab the primary file, if available - if(Json::requireBoolean(parent, "primary")) - break; - - i++; - } - - auto parent = files[i].toObject(); - if (parent.contains("url")) { - file.downloadUrl = Json::requireString(parent, "url"); - file.fileName = Json::requireString(parent, "filename"); + auto file = loadIndexedPackVersion(obj); + if(file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); - } } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format @@ -106,3 +110,61 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion +{ + ModPlatform::IndexedVersion file; + + file.addonId = Json::requireString(obj, "project_id"); + file.fileId = Json::requireString(obj, "id"); + file.date = Json::requireString(obj, "date_published"); + auto versionArray = Json::requireArray(obj, "game_versions"); + if (versionArray.empty()) { + return {}; + } + for (auto mcVer : versionArray) { + file.mcVersion.append(mcVer.toString()); + } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { + file.loaders.append(loader.toString()); + } + file.version = Json::requireString(obj, "name"); + + auto files = Json::requireArray(obj, "files"); + int i = 0; + + // Find correct file (needed in cases where one version may have multiple files) + // Will default to the last one if there's no primary (though I think Modrinth requires that + // at least one file is primary, idk) + // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed + while (i < files.count() - 1) { + auto parent = files[i].toObject(); + auto fileName = Json::requireString(parent, "filename"); + + // Grab the primary file, if available + if (Json::requireBoolean(parent, "primary")) + break; + + i++; + } + + auto parent = files[i].toObject(); + if (parent.contains("url")) { + file.downloadUrl = Json::requireString(parent, "url"); + file.fileName = Json::requireString(parent, "filename"); + auto hash_list = Json::requireObject(parent, "hashes"); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); + for (auto& hash_type : hash_types) { + if (hash_list.contains(hash_type)) { + file.hash = Json::requireString(hash_list, hash_type); + file.hash_type = hash_type; + break; + } + } + + return file; + } + + return {}; +} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 7f306f25..b7936204 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -25,9 +25,11 @@ namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance* inst); +auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 33116231..a4620df9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -64,8 +64,35 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj) { pack.extra.body = Json::ensureString(obj, "body"); pack.extra.projectUrl = QString("https://modrinth.com/modpack/%1").arg(Json::ensureString(obj, "slug")); + + pack.extra.issuesUrl = Json::ensureString(obj, "issues_url"); + if(pack.extra.issuesUrl.endsWith('/')) + pack.extra.issuesUrl.chop(1); + pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); + if(pack.extra.sourceUrl.endsWith('/')) + pack.extra.sourceUrl.chop(1); + pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); + if(pack.extra.wikiUrl.endsWith('/')) + pack.extra.wikiUrl.chop(1); + + pack.extra.discordUrl = Json::ensureString(obj, "discord_url"); + if(pack.extra.discordUrl.endsWith('/')) + pack.extra.discordUrl.chop(1); + + auto donate_arr = Json::ensureArray(obj, "donation_urls"); + for(auto d : donate_arr){ + auto d_obj = Json::requireObject(d); + + DonationData donate; + + donate.id = Json::ensureString(d_obj, "id"); + donate.platform = Json::ensureString(d_obj, "platform"); + donate.url = Json::ensureString(d_obj, "url"); + + pack.extra.donate.append(donate); + } pack.extraInfoLoaded = true; } @@ -95,19 +122,6 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } -auto validateDownloadUrl(QUrl url) -> bool -{ - static QSet<QString> domainWhitelist{ - "cdn.modrinth.com", - "github.com", - "raw.githubusercontent.com", - "gitlab.com" - }; - - auto domain = url.host(); - return domainWhitelist.contains(domain); -} - auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion { ModpackVersion file; @@ -137,9 +151,6 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto url = Json::requireString(parent, "url"); - if(!validateDownloadUrl(url)) - continue; - file.download_url = url; if(is_primary) break; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index e5fc9a70..035dc62e 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -40,6 +40,7 @@ #include <QByteArray> #include <QCryptographicHash> +#include <QQueue> #include <QString> #include <QUrl> #include <QVector> @@ -48,22 +49,32 @@ class MinecraftInstance; namespace Modrinth { -struct File -{ +struct File { QString path; QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; - // TODO: should this support multiple download URLs, like the JSON does? - QUrl download; + QQueue<QUrl> downloads; +}; + +struct DonationData { + QString id; + QString platform; + QString url; }; struct ModpackExtra { QString body; QString projectUrl; + + QString issuesUrl; QString sourceUrl; QString wikiUrl; + QString discordUrl; + + QList<DonationData> donate; + }; struct ModpackVersion { diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp new file mode 100644 index 00000000..0782b9f4 --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "Packwiz.h" + +#include <QDebug> +#include <QDir> +#include <QObject> + +#include "toml.h" +#include "FileSystem.h" + +#include "minecraft/mod/Mod.h" +#include "modplatform/ModIndex.h" + +namespace Packwiz { + +auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString +{ + QFile index_file(index_dir.absoluteFilePath(normalized_fname)); + + QString real_fname = normalized_fname; + if (!index_file.exists()) { + // Tries to get similar entries + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) { + real_fname = file_name; + break; + } + } + + if(should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)){ + qCritical() << "Could not find a match for a valid metadata file!"; + qCritical() << "File: " << normalized_fname; + return {}; + } + } + + return real_fname; +} + +// Helpers +static inline auto indexFileName(QString const& mod_name) -> QString +{ + if(mod_name.endsWith(".pw.toml")) + return mod_name; + return QString("%1.pw.toml").arg(mod_name); +} + +static ModPlatform::ProviderCapabilities ProviderCaps; + +// Helper functions for extracting data from the TOML file +auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString +{ + toml_datum_t var = toml_string_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + QString tmp = var.u.s; + free(var.u.s); + + return tmp; +} + +auto intEntry(toml_table_t* parent, const char* entry_name) -> int +{ + toml_datum_t var = toml_int_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + return var.u.i; +} + + +auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod +{ + Mod mod; + + mod.name = mod_pack.name; + mod.filename = mod_version.fileName; + + if(mod_pack.provider == ModPlatform::Provider::FLAME){ + mod.mode = "metadata:curseforge"; + } + else { + mod.mode = "url"; + mod.url = mod_version.downloadUrl; + } + + mod.hash_format = mod_version.hash_type; + mod.hash = mod_version.hash; + + mod.provider = mod_pack.provider; + mod.file_id = mod_version.fileId; + mod.project_id = mod_pack.addonId; + + return mod; +} + +auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod +{ + auto mod_name = internal_mod.name(); + + // Try getting metadata if it exists + Mod mod { getIndexForMod(index_dir, mod_name) }; + if(mod.isValid()) + return mod; + + qWarning() << QString("Tried to create mod metadata with a Mod without metadata!"); + + return {}; +} + +void V1::updateModIndex(QDir& index_dir, Mod& mod) +{ + if(!mod.isValid()){ + qCritical() << QString("Tried to update metadata of an invalid mod!"); + return; + } + + // Ensure the corresponding mod's info exists, and create it if not + + auto normalized_fname = indexFileName(mod.name); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + // There's already data on there! + // TODO: We should do more stuff here, as the user is likely trying to + // override a file. In this case, check versions and ask the user what + // they want to do! + if (index_file.exists()) { index_file.remove(); } + + if (!index_file.open(QIODevice::ReadWrite)) { + qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); + return; + } + + // Put TOML data into the file + QTextStream in_stream(&index_file); + auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); }; + + { + addToStream("name", mod.name); + addToStream("filename", mod.filename); + addToStream("side", mod.side); + + in_stream << QString("\n[download]\n"); + addToStream("mode", mod.mode); + addToStream("url", mod.url.toString()); + addToStream("hash-format", mod.hash_format); + addToStream("hash", mod.hash); + + in_stream << QString("\n[update]\n"); + in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); + switch(mod.provider){ + case(ModPlatform::Provider::FLAME): + in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); + in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); + break; + case(ModPlatform::Provider::MODRINTH): + addToStream("mod-id", mod.mod_id().toString()); + addToStream("version", mod.version().toString()); + break; + } + } + + index_file.close(); +} + +void V1::deleteModIndex(QDir& index_dir, QString& mod_name) +{ + auto normalized_fname = indexFileName(mod_name); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + if (real_fname.isEmpty()) + return; + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + if(!index_file.exists()){ + qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); + return; + } + + if(!index_file.remove()){ + qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); + } +} + +auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod +{ + Mod mod; + + auto normalized_fname = indexFileName(index_file_name); + auto real_fname = getRealIndexName(index_dir, normalized_fname, true); + if (real_fname.isEmpty()) + return {}; + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + if (!index_file.open(QIODevice::ReadOnly)) { + qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name); + return {}; + } + + toml_table_t* table = nullptr; + + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + char errbuf[200]; + auto file_bytearray = index_file.readAll(); + table = toml_parse(file_bytearray.data(), errbuf, sizeof(errbuf)); + + index_file.close(); + + if (!table) { + qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name)); + qWarning() << "Reason: " << QString(errbuf); + return {}; + } + + { // Basic info + mod.name = stringEntry(table, "name"); + mod.filename = stringEntry(table, "filename"); + mod.side = stringEntry(table, "side"); + } + + { // [download] info + toml_table_t* download_table = toml_table_in(table, "download"); + if (!download_table) { + qCritical() << QString("No [download] section found on mod metadata!"); + return {}; + } + + mod.mode = stringEntry(download_table, "mode"); + mod.url = stringEntry(download_table, "url"); + mod.hash_format = stringEntry(download_table, "hash-format"); + mod.hash = stringEntry(download_table, "hash"); + } + + { // [update] info + using Provider = ModPlatform::Provider; + + toml_table_t* update_table = toml_table_in(table, "update"); + if (!update_table) { + qCritical() << QString("No [update] section found on mod metadata!"); + return {}; + } + + toml_table_t* mod_provider_table = nullptr; + if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::FLAME)))) { + mod.provider = Provider::FLAME; + mod.file_id = intEntry(mod_provider_table, "file-id"); + mod.project_id = intEntry(mod_provider_table, "project-id"); + } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::MODRINTH)))) { + mod.provider = Provider::MODRINTH; + mod.mod_id() = stringEntry(mod_provider_table, "mod-id"); + mod.version() = stringEntry(mod_provider_table, "version"); + } else { + qCritical() << QString("No mod provider on mod metadata!"); + return {}; + } + + } + + toml_free(table); + + return mod; +} + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h new file mode 100644 index 00000000..3c99769c --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include "modplatform/ModIndex.h" + +#include <QString> +#include <QUrl> +#include <QVariant> + +struct toml_table_t; +class QDir; + +// Mod from launcher/minecraft/mod/Mod.h +class Mod; + +namespace Packwiz { + +auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; + +auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString; +auto intEntry(toml_table_t* parent, const char* entry_name) -> int; + +class V1 { + public: + struct Mod { + QString name {}; + QString filename {}; + // FIXME: make side an enum + QString side {"both"}; + + // [download] + QString mode {}; + QUrl url {}; + QString hash_format {}; + QString hash {}; + + // [update] + ModPlatform::Provider provider {}; + QVariant file_id {}; + QVariant project_id {}; + + public: + // This is a totally heuristic, but should work for now. + auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); } + + // Different providers can use different names for the same thing + // Modrinth-specific + auto mod_id() -> QVariant& { return project_id; } + auto version() -> QVariant& { return file_id; } + }; + + /* Generates the object representing the information in a mod.pw.toml file via + * its common representation in the launcher, when downloading mods. + * */ + static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; + /* Generates the object representing the information in a mod.pw.toml file via + * its common representation in the launcher. + * */ + static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod; + + /* Updates the mod index for the provided mod. + * This creates a new index if one does not exist already + * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one. + * */ + static void updateModIndex(QDir& index_dir, Mod& mod); + + /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QString& mod_name); + + /* Gets the metadata for a mod with a particular name. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod; +}; + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp new file mode 100644 index 00000000..3d47f9f7 --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <QTemporaryDir> +#include <QTest> + +#include "Packwiz.h" +#include "TestUtil.h" + +class PackwizTest : public QObject { + Q_OBJECT + + private slots: + // Files taken from https://github.com/packwiz/packwiz-example-pack + void loadFromFile_Modrinth() + { + QString source = QFINDTESTDATA("testdata"); + + QDir index_dir(source); + QString name_mod("borderless-mining.pw.toml"); + QVERIFY(index_dir.entryList().contains(name_mod)); + + auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + + QVERIFY(metadata.isValid()); + + QCOMPARE(metadata.name, "Borderless Mining"); + QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); + QCOMPARE(metadata.side, "client"); + + QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); + QCOMPARE(metadata.hash_format, "sha512"); + QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"); + + QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH); + QCOMPARE(metadata.version(), "ug2qKTPR"); + QCOMPARE(metadata.mod_id(), "kYq5qkSL"); + } + + void loadFromFile_Curseforge() + { + QString source = QFINDTESTDATA("testdata"); + + QDir index_dir(source); + QString name_mod("screenshot-to-clipboard-fabric.pw.toml"); + QVERIFY(index_dir.entryList().contains(name_mod)); + + // Try without the .pw.toml at the end + name_mod.chop(8); + + auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + + QVERIFY(metadata.isValid()); + + QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); + QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); + QCOMPARE(metadata.side, "both"); + + QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); + QCOMPARE(metadata.hash_format, "murmur2"); + QCOMPARE(metadata.hash, "1781245820"); + + QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME); + QCOMPARE(metadata.file_id, 3509043); + QCOMPARE(metadata.project_id, 327154); + } +}; + +QTEST_GUILESS_MAIN(PackwizTest) + +#include "Packwiz_test.moc" diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml new file mode 100644 index 00000000..16545fd4 --- /dev/null +++ b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml @@ -0,0 +1,13 @@ +name = "Borderless Mining" +filename = "borderless-mining-1.1.1+1.18.jar" +side = "client" + +[download] +url = "https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar" +hash-format = "sha512" +hash = "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d" + +[update] +[update.modrinth] +mod-id = "kYq5qkSL" +version = "ug2qKTPR" diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml new file mode 100644 index 00000000..87d70ada --- /dev/null +++ b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml @@ -0,0 +1,13 @@ +name = "Screenshot to Clipboard (Fabric)" +filename = "screenshot-to-clipboard-1.0.7-fabric.jar" +side = "both" + +[download] +url = "https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar" +hash-format = "murmur2" +hash = "1781245820" + +[update] +[update.curseforge] +file-id = 3509043 +project-id = 327154 diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 782fb9b2..471b4a2f 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -185,13 +185,22 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); } } - else if (libraryName.startsWith("net.minecraftforge:minecraftforge:")) + else { - components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2)); - } - else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) - { - components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2)); + static QStringList possibleLoaders{ + "net.minecraftforge:minecraftforge:", + "net.fabricmc:fabric-loader:", + "org.quiltmc:quilt-loader:" + }; + for (const auto& loader : possibleLoaders) + { + if (libraryName.startsWith(loader)) + { + auto loaderComponent = loader.chopped(1).replace(":", "."); + components->setComponentVersion(loaderComponent, libraryName.section(':', 2)); + break; + } + } } } } diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 966d4126..d93eb088 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -116,7 +116,7 @@ void Download::executeTask() return; } - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (request.url().host().contains("api.curseforge.com")) { request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); }; diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 3855190a..7438e1a1 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> * Copyright (C) 2022 Swirl <swurl@swurl.xyz> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,6 +44,8 @@ #include <QJsonArray> #include <QJsonDocument> #include <QFile> +#include <QHttpPart> +#include <QUrlQuery> std::array<PasteUpload::PasteTypeInfo, 4> PasteUpload::PasteTypes = { {{"0x0.st", "https://0x0.st", ""}, @@ -71,7 +74,7 @@ void PasteUpload::executeTask() QNetworkRequest request{QUrl(m_uploadUrl)}; QNetworkReply *rep{}; - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); switch (m_pasteType) { case NullPointer: { @@ -91,7 +94,7 @@ void PasteUpload::executeTask() break; } case Hastebin: { - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); rep = APPLICATION->network()->post(request, m_text); break; } diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index bbd27390..c9942a8d 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -173,7 +173,7 @@ namespace Net { return; } - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (request.url().host().contains("api.curseforge.com")) { request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); } diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 7afdc5cc..04e26ea2 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -55,7 +55,7 @@ void ImgurAlbumCreation::executeTask() { m_state = State::Running; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index fbcfb95f..9aeb6fb8 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -35,6 +35,7 @@ #include "ImgurUpload.h" #include "BuildConfig.h" +#include "Application.h" #include <QNetworkRequest> #include <QHttpMultiPart> @@ -56,7 +57,7 @@ void ImgurUpload::executeTask() finished = false; m_state = Task::State::Running; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 1573e476..7f03ad2e 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -33,11 +33,22 @@ void SequentialTask::executeTask() bool SequentialTask::abort() { - bool succeeded = true; - for (auto& task : m_queue) { - if (!task->abort()) succeeded = false; + if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) { + if(m_currentIndex == -1) { + // Don't call emitAborted() here, we want to bypass the 'is the task running' check + emit aborted(); + emit finished(); + } + m_queue.clear(); + return true; } + bool succeeded = m_queue[m_currentIndex]->abort(); + m_queue.clear(); + + if(succeeded) + emitAborted(); + return succeeded; } @@ -53,12 +64,18 @@ void SequentialTask::startNext() return; } Task::Ptr next = m_queue[m_currentIndex]; + connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString))); + connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); + connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(stepStatus(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64))); - connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + next->start(); } @@ -68,7 +85,7 @@ void SequentialTask::subTaskFailed(const QString& msg) } void SequentialTask::subTaskStatus(const QString& msg) { - setStepStatus(m_queue[m_currentIndex]->getStatus()); + setStepStatus(msg); } void SequentialTask::subTaskProgress(qint64 current, qint64 total) { @@ -76,7 +93,7 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total) setProgress(0, 100); return; } - setProgress(m_currentIndex, m_queue.count()); + setProgress(m_currentIndex + 1, m_queue.count()); m_stepProgress = current; m_stepTotalProgress = total; diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index 5b3c0111..e10cb6f7 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -32,13 +32,10 @@ slots: void subTaskStatus(const QString &msg); void subTaskProgress(qint64 current, qint64 total); -signals: - void stepStatus(QString status); +protected: + void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; -private: - void setStepStatus(QString status) { m_step_status = status; }; - -private: +protected: QString m_name; QString m_step_status; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index f7765c3d..aafaf68c 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -92,6 +92,7 @@ class Task : public QObject { void aborted(); void failed(QString reason); void status(QString status); + void stepStatus(QString status); public slots: virtual void start(); diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 320f1502..b1ea5ee9 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +39,7 @@ #include <QClipboard> #include <QApplication> #include <QFileDialog> +#include <QStandardPaths> #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/CustomMessageBox.h" diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7e152b96..210442df 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1,21 +1,42 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: * - * Authors: Andrew Okin - * Peterix - * Orochimarufan <orochimarufan.x3@gmail.com> + * Copyright 2013-2021 MultiMC Contributors * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Authors: Andrew Okin + * Peterix + * Orochimarufan <orochimarufan.x3@gmail.com> * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + #include "Application.h" #include "BuildConfig.h" @@ -1010,6 +1031,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } +#ifdef LAUNCHER_WITH_UPDATER if(BuildConfig.UPDATER_ENABLED) { bool updatesAllowed = APPLICATION->updatesAreAllowed(); @@ -1028,6 +1050,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); } } +#endif setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); @@ -1337,6 +1360,7 @@ void MainWindow::repopulateAccountsMenu() ui->profileMenu->addAction(ui->actionManageAccounts); } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::updatesAllowedChanged(bool allowed) { if(!BuildConfig.UPDATER_ENABLED) @@ -1345,6 +1369,7 @@ void MainWindow::updatesAllowedChanged(bool allowed) } ui->actionCheckUpdate->setEnabled(allowed); } +#endif /* * Assumes the sender is a QAction @@ -1450,6 +1475,7 @@ void MainWindow::updateNewsLabel() } } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::updateAvailable(GoUpdate::Status status) { if(!APPLICATION->updatesAreAllowed()) @@ -1475,6 +1501,7 @@ void MainWindow::updateNotAvailable() UpdateDialog dlg(false, this); dlg.exec(); } +#endif QList<int> stringToIntList(const QString &string) { @@ -1496,6 +1523,7 @@ QString intListToString(const QList<int> &list) return slist.join(','); } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::downloadUpdates(GoUpdate::Status status) { if(!APPLICATION->updatesAreAllowed()) @@ -1529,6 +1557,7 @@ void MainWindow::downloadUpdates(GoUpdate::Status status) CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show(); } } +#endif void MainWindow::onCatToggled(bool state) { @@ -1841,6 +1870,7 @@ void MainWindow::on_actionConfig_Folder_triggered() } } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::checkForUpdates() { if(BuildConfig.UPDATER_ENABLED) @@ -1853,6 +1883,7 @@ void MainWindow::checkForUpdates() qWarning() << "Updater not set up. Cannot check for updates."; } } +#endif void MainWindow::on_actionSettings_triggered() { @@ -2101,6 +2132,9 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & selectionBad(); return; } + if (m_selectedInstance) { + disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); if (m_selectedInstance) @@ -2127,6 +2161,8 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & updateToolsMenu(); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); + + connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); } else { @@ -2216,3 +2252,9 @@ void MainWindow::updateStatusCenter() m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); } } + +void MainWindow::refreshCurrentInstance(bool running) +{ + auto current = view->selectionModel()->currentIndex(); + instanceChanged(current, current); +} diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 2032acba..6c64756f 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -1,16 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Andrew Okin + * Peterix + * Orochimarufan <orochimarufan.x3@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -54,7 +78,9 @@ public: void checkInstancePathForProblems(); +#ifdef LAUNCHER_WITH_UPDATER void updatesAllowedChanged(bool allowed); +#endif void droppedURLs(QList<QUrl> urls); signals: @@ -100,7 +126,9 @@ private slots: void on_actionViewCentralModsFolder_triggered(); +#ifdef LAUNCHER_WITH_UPDATER void checkForUpdates(); +#endif void on_actionSettings_triggered(); @@ -167,9 +195,11 @@ private slots: void startTask(Task *task); +#ifdef LAUNCHER_WITH_UPDATER void updateAvailable(GoUpdate::Status status); void updateNotAvailable(); +#endif void defaultAccountChanged(); @@ -179,10 +209,12 @@ private slots: void updateNewsLabel(); +#ifdef LAUNCHER_WITH_UPDATER /*! * Runs the DownloadTask and installs updates. */ void downloadUpdates(GoUpdate::Status status); +#endif void konamiTriggered(); @@ -192,6 +224,8 @@ private slots: void keyReleaseEvent(QKeyEvent *event) override; #endif + void refreshCurrentInstance(bool running); + private: void retranslateUi(); diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 305e85c0..f01c9c07 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -77,18 +77,20 @@ void ModDownloadDialog::confirm() auto keys = modTask.keys(); keys.sort(Qt::CaseInsensitive); - auto confirm_dialog = ReviewMessageBox::create( - this, - tr("Confirm mods to download") - ); + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download")); - for(auto& task : keys){ - confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename()); + for (auto& task : keys) { + confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() }); } - connect(confirm_dialog, &QDialog::accepted, this, &ModDownloadDialog::accept); + if (confirm_dialog->exec()) { + auto deselected = confirm_dialog->deselectedMods(); + for (auto name : deselected) { + modTask.remove(name); + } - confirm_dialog->open(); + this->accept(); + } } void ModDownloadDialog::accept() @@ -132,6 +134,12 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena return iter != modTask.end() && (iter.value()->getFilename() == filename); } +bool ModDownloadDialog::isModSelected(const QString &name) const +{ + auto iter = modTask.find(name); + return iter != modTask.end(); +} + ModDownloadDialog::~ModDownloadDialog() { } diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 782dc361..5c565ad3 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -32,6 +32,7 @@ public: void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); void removeSelectedMod(const QString & name = QString()); bool isModSelected(const QString & name, const QString & filename) const; + bool isModSelected(const QString & name) const; const QList<ModDownloadTask*> getTasks(); const std::shared_ptr<ModFolderModel> &mods; @@ -41,8 +42,6 @@ public slots: void accept() override; void reject() override; -//private slots: - private: Ui::ModDownloadDialog *ui = nullptr; PageContainer * m_container = nullptr; diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 648bd88b..e5226016 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -16,12 +16,12 @@ #include "ProgressDialog.h" #include "ui_ProgressDialog.h" -#include <QKeyEvent> #include <QDebug> +#include <QKeyEvent> #include "tasks/Task.h" -ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog) +ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog) { ui->setupUi(this); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -44,6 +44,7 @@ void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); task->abort(); + QDialog::reject(); } ProgressDialog::~ProgressDialog() @@ -53,24 +54,22 @@ ProgressDialog::~ProgressDialog() void ProgressDialog::updateSize() { - QSize qSize = QSize(480, minimumSizeHint().height()); + QSize qSize = QSize(480, minimumSizeHint().height()); resize(qSize); - setFixedSize(qSize); + setFixedSize(qSize); } -int ProgressDialog::execWithTask(Task *task) +int ProgressDialog::execWithTask(Task* task) { this->task = task; QDialog::DialogCode result; - if(!task) - { + if (!task) { qDebug() << "Programmer error: progress dialog created with null task."; return Accepted; } - if(handleImmediateResult(result)) - { + if (handleImmediateResult(result)) { return result; } @@ -78,58 +77,51 @@ int ProgressDialog::execWithTask(Task *task) connect(task, SIGNAL(started()), SLOT(onTaskStarted())); connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); - connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &))); + connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&))); + connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&))); connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); + m_is_multi_step = task->isMultiStep(); - if(!m_is_multi_step){ + if (!m_is_multi_step) { ui->globalStatusLabel->setHidden(true); ui->globalProgressBar->setHidden(true); } // if this didn't connect to an already running task, invoke start - if(!task->isRunning()) - { + if (!task->isRunning()) { task->start(); } - if(task->isRunning()) - { + if (task->isRunning()) { changeProgress(task->getProgress(), task->getTotalProgress()); changeStatus(task->getStatus()); return QDialog::exec(); - } - else if(handleImmediateResult(result)) - { + } else if (handleImmediateResult(result)) { return result; - } - else - { + } else { return QDialog::Rejected; } } // TODO: only provide the unique_ptr overloads -int ProgressDialog::execWithTask(std::unique_ptr<Task> &&task) +int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } -int ProgressDialog::execWithTask(std::unique_ptr<Task> &task) +int ProgressDialog::execWithTask(std::unique_ptr<Task>& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } -bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) { - if(task->isFinished()) - { - if(task->wasSuccessful()) - { + if (task->isFinished()) { + if (task->wasSuccessful()) { result = QDialog::Accepted; - } - else - { + } else { result = QDialog::Rejected; } return true; @@ -137,14 +129,12 @@ bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) return false; } -Task *ProgressDialog::getTask() +Task* ProgressDialog::getTask() { return task; } -void ProgressDialog::onTaskStarted() -{ -} +void ProgressDialog::onTaskStarted() {} void ProgressDialog::onTaskFailed(QString failure) { @@ -156,10 +146,11 @@ void ProgressDialog::onTaskSucceeded() accept(); } -void ProgressDialog::changeStatus(const QString &status) +void ProgressDialog::changeStatus(const QString& status) { + ui->globalStatusLabel->setText(task->getStatus()); ui->statusLabel->setText(task->getStepStatus()); - ui->globalStatusLabel->setText(status); + updateSize(); } @@ -168,27 +159,22 @@ void ProgressDialog::changeProgress(qint64 current, qint64 total) ui->globalProgressBar->setMaximum(total); ui->globalProgressBar->setValue(current); - if(!m_is_multi_step){ + if (!m_is_multi_step) { ui->taskProgressBar->setMaximum(total); ui->taskProgressBar->setValue(current); - } - else{ + } else { ui->taskProgressBar->setMaximum(task->getStepProgress()); ui->taskProgressBar->setValue(task->getStepTotalProgress()); } } -void ProgressDialog::keyPressEvent(QKeyEvent *e) +void ProgressDialog::keyPressEvent(QKeyEvent* e) { - if(ui->skipButton->isVisible()) - { - if (e->key() == Qt::Key_Escape) - { + if (ui->skipButton->isVisible()) { + if (e->key() == Qt::Key_Escape) { on_skipButton_clicked(true); return; - } - else if(e->key() == Qt::Key_Tab) - { + } else if (e->key() == Qt::Key_Tab) { ui->skipButton->setFocusPolicy(Qt::StrongFocus); ui->skipButton->setFocus(); ui->skipButton->setAutoDefault(true); @@ -199,14 +185,11 @@ void ProgressDialog::keyPressEvent(QKeyEvent *e) QDialog::keyPressEvent(e); } -void ProgressDialog::closeEvent(QCloseEvent *e) +void ProgressDialog::closeEvent(QCloseEvent* e) { - if (task && task->isRunning()) - { + if (task && task->isRunning()) { e->ignore(); - } - else - { + } else { QDialog::closeEvent(e); } } diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui index bf119a78..34ab71e3 100644 --- a/launcher/ui/dialogs/ProgressDialog.ui +++ b/launcher/ui/dialogs/ProgressDialog.ui @@ -40,6 +40,12 @@ </item> <item row="2" column="0"> <widget class="QLabel" name="statusLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="text"> <string>Task Status...</string> </property> diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 2bfd02e0..c92234a4 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -5,6 +5,9 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } ReviewMessageBox::~ReviewMessageBox() @@ -17,15 +20,33 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) return new ReviewMessageBox(parent, title, icon); } -void ReviewMessageBox::appendMod(const QString& name, const QString& filename) +void ReviewMessageBox::appendMod(ModInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); - itemTop->setText(0, name); + itemTop->setCheckState(0, Qt::CheckState::Checked); + itemTop->setText(0, info.name); auto filenameItem = new QTreeWidgetItem(itemTop); - filenameItem->setText(0, tr("Filename: %1").arg(filename)); + filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); itemTop->insertChildren(0, { filenameItem }); ui->modTreeWidget->addTopLevelItem(itemTop); } + +auto ReviewMessageBox::deselectedMods() -> QStringList +{ + QStringList list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 0; item != nullptr; ++i) { + if (item->checkState(0) == Qt::CheckState::Unchecked) { + list.append(item->text(0)); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 48742cd9..9cfa679a 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -6,17 +6,23 @@ namespace Ui { class ReviewMessageBox; } -class ReviewMessageBox final : public QDialog { +class ReviewMessageBox : public QDialog { Q_OBJECT public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; - void appendMod(const QString& name, const QString& filename); + using ModInformation = struct { + QString name; + QString filename; + }; + + void appendMod(ModInformation&& info); + auto deselectedMods() -> QStringList; ~ReviewMessageBox(); - private: + protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); Ui::ReviewMessageBox* ui; diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui index d04f3b3f..ab3bcc2f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.ui +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>300</height> + <width>500</width> + <height>350</height> </rect> </property> <property name="windowTitle"> @@ -20,24 +20,7 @@ <bool>true</bool> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>You're about to download the following mods:</string> - </property> - </widget> - </item> <item row="2" column="0"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - <item row="1" column="0"> <widget class="QTreeWidget" name="modTreeWidget"> <property name="alternatingRowColors"> <bool>true</bool> @@ -58,41 +41,33 @@ </column> </widget> </item> + <item row="1" column="0"> + <widget class="QLabel" name="explainLabel"> + <property name="text"> + <string>You're about to download the following mods:</string> + </property> + </widget> + </item> + <item row="5" column="0" rowspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="onlyCheckedLabel"> + <property name="text"> + <string>Only mods with a check will be downloaded!</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> <resources/> - <connections> - <connection> - <sender>buttonBox</sender> - <signal>accepted()</signal> - <receiver>ReviewMessageBox</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>200</x> - <y>265</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>buttonBox</sender> - <signal>rejected()</signal> - <receiver>ReviewMessageBox</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>200</x> - <y>265</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - </connections> + <connections/> </ui> diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 5d812d07..b889e6f7 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -75,9 +75,9 @@ 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->tabWidget->tabBar()->hide(); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); + ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); loadSettings(); @@ -139,6 +139,8 @@ void APIPage::loadSettings() ui->metaURL->setText(metaURL); QString curseKey = s->get("CFKeyOverride").toString(); ui->curseKey->setText(curseKey); + QString customUserAgent = s->get("UserAgentOverride").toString(); + ui->userAgentLineEdit->setText(customUserAgent); } void APIPage::applySettings() @@ -167,6 +169,7 @@ void APIPage::applySettings() s->set("MetaURLOverride", metaURL); QString curseKey = ui->curseKey->text(); s->set("CFKeyOverride", curseKey); + s->set("UserAgentOverride", ui->userAgentLineEdit->text()); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 5c927391..5327771c 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -30,7 +30,7 @@ </property> <widget class="QWidget" name="tab"> <attribute name="title"> - <string notr="true">Tab 1</string> + <string notr="true">Services</string> </attribute> <layout class="QVBoxLayout" name="verticalLayout_2"> <item> @@ -86,15 +86,15 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox_msa"> + <widget class="QGroupBox" name="groupBox_meta"> <property name="title"> - <string>&Microsoft Authentication</string> + <string>Meta&data Server</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> + <layout class="QVBoxLayout" name="verticalLayout_5"> <item> - <widget class="QLabel" name="label_3"> + <widget class="QLabel" name="label_5"> <property name="text"> - <string>Note: you probably don't need to set this if logging in via Microsoft Authentication already works.</string> + <string>You can set this to a third-party metadata server to use patched libraries or other hacks.</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -105,16 +105,16 @@ </widget> </item> <item> - <widget class="QLineEdit" name="msaClientID"> + <widget class="QLineEdit" name="metaURL"> <property name="placeholderText"> - <string>(Default)</string> + <string/> </property> </widget> </item> <item> - <widget class="QLabel" name="label_4"> + <widget class="QLabel" name="label_6"> <property name="text"> - <string>Enter a custom client ID for Microsoft Authentication here. </string> + <string>Enter a custom URL for meta here.</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -131,15 +131,35 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox_meta"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>API Keys</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> + <widget class="QGroupBox" name="groupBox_msa"> <property name="title"> - <string>Meta&data Server</string> + <string>&Microsoft Authentication</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_5"> + <layout class="QVBoxLayout" name="verticalLayout_4"> <item> - <widget class="QLabel" name="label_5"> + <widget class="QLabel" name="label_3"> <property name="text"> - <string>You can set this to a third-party metadata server to use patched libraries or other hacks.</string> + <string>Note: you probably don't need to set this if logging in via Microsoft Authentication already works.</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -150,16 +170,16 @@ </widget> </item> <item> - <widget class="QLineEdit" name="metaURL"> + <widget class="QLineEdit" name="msaClientID"> <property name="placeholderText"> - <string/> + <string>(Default)</string> </property> </widget> </item> <item> - <widget class="QLabel" name="label_6"> + <widget class="QLabel" name="label_4"> <property name="text"> - <string>Enter a custom URL for meta here.</string> + <string>Enter a custom client ID for Microsoft Authentication here. </string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -183,25 +203,15 @@ <property name="title"> <string>&CurseForge Core API</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> <widget class="QLabel" name="label_8"> <property name="text"> <string>Note: you probably don't need to set this if CurseForge already works.</string> </property> </widget> </item> - <item> - <widget class="QLineEdit" name="curseKey"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="placeholderText"> - <string>(Default)</string> - </property> - </widget> - </item> - <item> + <item row="2" column="0"> <widget class="QLabel" name="label_7"> <property name="text"> <string>Enter a custom API Key for CurseForge here. </string> @@ -217,6 +227,16 @@ </property> </widget> </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="curseKey"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="placeholderText"> + <string>(Default)</string> + </property> + </widget> + </item> </layout> </widget> </item> @@ -235,6 +255,51 @@ </item> </layout> </widget> + <widget class="QWidget" name="tab_3"> + <attribute name="title"> + <string>Miscellaneous</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QGroupBox" name="groupBox_ua"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="title"> + <string>User Agent</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QLineEdit" name="userAgentLineEdit"/> + </item> + <item> + <widget class="QLabel" name="userAgentLabel"> + <property name="text"> + <string>Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> </widget> </item> </layout> diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index 485d7fd4..fcd174bd 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index 9b321170..2fd4ab0d 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index af2e2cd1..edbf609f 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -78,6 +78,7 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch m_languageModel = APPLICATION->translations(); loadSettings(); +#ifdef LAUNCHER_WITH_UPDATER if(BuildConfig.UPDATER_ENABLED) { QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList); @@ -90,11 +91,9 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch { APPLICATION->updateChecker()->updateChanList(false); } + ui->updateSettingsBox->setHidden(false); } - else - { - ui->updateSettingsBox->setHidden(true); - } +#endif connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); } @@ -184,6 +183,12 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() } } +void LauncherPage::on_metadataDisableBtn_clicked() +{ + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); +} + +#ifdef LAUNCHER_WITH_UPDATER void LauncherPage::refreshUpdateChannelList() { // Stop listening for selection changes. It's going to change a lot while we update it and @@ -255,6 +260,7 @@ void LauncherPage::refreshUpdateChannelDesc() m_currentUpdateChannel = selected.id; } } +#endif void LauncherPage::applySettings() { @@ -338,6 +344,9 @@ void LauncherPage::applySettings() s->set("InstSortMode", "Name"); break; } + + // Mods + s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -440,6 +449,10 @@ void LauncherPage::loadSettings() { ui->sortByNameBtn->setChecked(true); } + + // Mods + ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } void LauncherPage::refreshFontPreview() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index bbf5d2fe..ccfd7e9e 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -88,7 +88,9 @@ slots: void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); + void on_metadataDisableBtn_clicked(); +#ifdef LAUNCHER_WITH_UPDATER /*! * Updates the list of update channels in the combo box. */ @@ -99,13 +101,13 @@ slots: */ void refreshUpdateChannelDesc(); + void updateChannelSelectionChanged(int index); +#endif /*! * Updates the font preview */ void refreshFontPreview(); - void updateChannelSelectionChanged(int index); - private: Ui::LauncherPage *ui; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index ae7eb73f..ceb68c5b 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -50,6 +50,9 @@ <property name="title"> <string>Update Settings</string> </property> + <property name="visible"> + <bool>false</bool> + </property> <layout class="QVBoxLayout" name="verticalLayout_7"> <item> <widget class="QCheckBox" name="autoUpdateCheckBox"> @@ -94,19 +97,13 @@ <string>Folders</string> </property> <layout class="QGridLayout" name="foldersBoxLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="labelInstDir"> + <item row="1" column="2"> + <widget class="QToolButton" name="modsDirBrowseBtn"> <property name="text"> - <string>I&nstances:</string> - </property> - <property name="buddy"> - <cstring>instDirTextBox</cstring> + <string notr="true">...</string> </property> </widget> </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="instDirTextBox"/> - </item> <item row="0" column="2"> <widget class="QToolButton" name="instDirBrowseBtn"> <property name="text"> @@ -114,43 +111,78 @@ </property> </widget> </item> - <item row="1" column="0"> - <widget class="QLabel" name="labelModsDir"> + <item row="2" column="2"> + <widget class="QToolButton" name="iconsDirBrowseBtn"> <property name="text"> - <string>&Mods:</string> + <string notr="true">...</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="instDirTextBox"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="labelIconsDir"> + <property name="text"> + <string>&Icons:</string> </property> <property name="buddy"> - <cstring>modsDirTextBox</cstring> + <cstring>iconsDirTextBox</cstring> </property> </widget> </item> <item row="1" column="1"> <widget class="QLineEdit" name="modsDirTextBox"/> </item> - <item row="1" column="2"> - <widget class="QToolButton" name="modsDirBrowseBtn"> + <item row="0" column="0"> + <widget class="QLabel" name="labelInstDir"> <property name="text"> - <string notr="true">...</string> + <string>I&nstances:</string> + </property> + <property name="buddy"> + <cstring>instDirTextBox</cstring> </property> </widget> </item> <item row="2" column="1"> <widget class="QLineEdit" name="iconsDirTextBox"/> </item> - <item row="2" column="0"> - <widget class="QLabel" name="labelIconsDir"> + <item row="1" column="0"> + <widget class="QLabel" name="labelModsDir"> <property name="text"> - <string>&Icons:</string> + <string>&Mods:</string> </property> <property name="buddy"> - <cstring>iconsDirTextBox</cstring> + <cstring>modsDirTextBox</cstring> </property> </widget> </item> - <item row="2" column="2"> - <widget class="QToolButton" name="iconsDirBrowseBtn"> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="modsBox"> + <property name="title"> + <string>Mods</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="metadataDisableBtn"> + <property name="toolTip"> + <string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string> + </property> <property name="text"> - <string notr="true">...</string> + <string>Disable using metadata for mods?</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="metadataWarningLabel"> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some upcoming QoL features, such as mod updating!</span></p></body></html></string> + </property> + <property name="wordWrap"> + <bool>true</bool> </property> </widget> </item> diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 30a8735f..a6c98c08 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,6 +40,7 @@ #include "Application.h" #include <QIcon> +#include <QIdentityProxyModel> #include <QScrollBar> #include <QShortcut> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 5574f9d2..d929a0ea 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -402,6 +403,10 @@ void ModFolderPage::on_actionInstall_mods_triggered() 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(); } @@ -411,6 +416,7 @@ void ModFolderPage::on_actionInstall_mods_triggered() for (auto task : mdownload.getTasks()) { tasks->addTask(task); } + ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 72e2d404..2dd44e85 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -41,6 +41,7 @@ #include "ui/pages/BasePage.h" #include <Application.h> +#include <QSortFilterProxyModel> class ModFolderModel; namespace Ui diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 2cf17b32..51163e28 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index c22706af..c34c9755 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -35,6 +35,7 @@ #pragma once +#include <QItemSelection> #include <QMainWindow> #include "ui/pages/BasePage.h" diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 2af6164c..c3bde612 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,6 +48,7 @@ #include <QFileSystemWatcher> #include <QMenu> +#include <QTimer> static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 76725539..ff30dd82 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,6 +47,7 @@ #include <QInputDialog> #include <QProcess> #include <Qt> +#include <QSortFilterProxyModel> #include "tools/MCEditTool.h" #include "FileSystem.h" diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index b3ed1b73..0b8577b1 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -110,11 +110,13 @@ void ImportPage::updateState() { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); - // mrpack is a modrinth pack // Allow non-latin people to use ZIP files! - auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); - if(fi.exists() && (zip || fi.suffix() == "mrpack")) + bool isZip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); + // mrpack is a modrinth pack + bool isMRPack = fi.suffix() == "mrpack"; + + if(fi.exists() && (isZip || isMRPack)) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); @@ -149,7 +151,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); - filter += ";;" + tr("Modrinth pack (*.mrpack)"); + //: Option for filtering for *.mrpack files when importing + filter += ";;" + tr("Modrinth pack") + " (*.mrpack)"; const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 9dd8f737..98eec31c 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -38,27 +38,44 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant } ModPlatform::IndexedPack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::ToolTipRole) { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); - return edit; + switch (role) { + case Qt::DisplayRole: { + return pack.name; } - return pack.description; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.logoName)) { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + // un-const-ify this + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::FontRole: { + QFont font; + if (m_parent->getDialog()->isModSelected(pack.name)) { + font.setBold(true); + font.setUnderline(true); + } + + return font; + } + default: + break; } return {}; @@ -79,6 +96,11 @@ void ListModel::performPaginatedSearch() this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); } +void ListModel::requestModInfo(ModPlatform::IndexedPack& current) +{ + m_parent->apiProvider()->getModInfo(this, current); +} + void ListModel::refresh() { if (jobPtr) { @@ -225,6 +247,21 @@ void ListModel::searchRequestFailed(QString reason) } } +void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack) +{ + qDebug() << "Loading mod info"; + + try { + auto obj = Json::requireObject(doc); + loadExtraPackInfo(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); + } + + m_parent->updateUi(); +} + void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { auto& current = m_parent->getCurrent(); diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index d460cff2..dd22407c 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -36,9 +36,11 @@ class ListModel : public QAbstractListModel { void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, const int sort, const bool filter_changed); + void requestModInfo(ModPlatform::IndexedPack& current); void requestModVersions(const ModPlatform::IndexedPack& current); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); @@ -49,6 +51,8 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc); void searchRequestFailed(QString reason); + void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack); + void versionRequestSucceeded(QJsonDocument doc, QString addonId); protected slots: diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index ad36cf2f..200fe59e 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,4 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ModPage.h" +#include "Application.h" #include "ui_ModPage.h" #include <QKeyEvent> @@ -94,28 +130,6 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!first.isValid()) { return; } current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - - if (!current.authors.empty()) { - auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { - if (author.url.isEmpty()) { return author.name; } - return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "<br>" + tr(" by ") + authorStrs.join(", "); - } - text += "<br><br>"; - - ui->packDescription->setHtml(text + current.description); if (!current.versionsLoaded) { qDebug() << QString("Loading %1 mod versions").arg(debugName()); @@ -132,6 +146,13 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) updateSelectionButton(); } + + if(!current.extraDataLoaded){ + qDebug() << QString("Loading %1 mod info").arg(debugName()); + listModel->requestModInfo(current); + } + + updateUi(); } void ModPage::onVersionSelectionChanged(QString data) @@ -150,7 +171,8 @@ void ModPage::onModSelected() if (dialog->isModSelected(current.name, version.fileName)) { dialog->removeSelectedMod(current.name); } else { - dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed)); } updateSelectionButton(); @@ -207,3 +229,61 @@ void ModPage::updateSelectionButton() ui->modSelectionButton->setText(tr("Deselect mod for download")); } } + +void ModPage::updateUi() +{ + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + + if (!current.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { return author.name; } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + + + if(current.extraDataLoaded) { + if (!current.extraData.donate.isEmpty()) { + text += "<br><br>" + tr("Donate information: "); + auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { + return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current.extraData.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + if (!current.extraData.issuesUrl.isEmpty() + || !current.extraData.sourceUrl.isEmpty() + || !current.extraData.wikiUrl.isEmpty() + || !current.extraData.discordUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current.extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>"; + if (!current.extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>"; + if (!current.extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>"; + if (!current.extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>"; + } + + text += "<hr>"; + + ui->packDescription->setHtml(text + current.description); +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 0e658a8d..cf00e16e 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -36,11 +36,14 @@ class ModPage : public QWidget, public BasePage { void retranslate() override; + void updateUi(); + auto shouldDisplay() const -> bool override = 0; virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; - auto apiProvider() const -> const ModAPI* { return api.get(); }; + auto apiProvider() -> ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; } + auto getDialog() const -> const ModDownloadDialog* { return dialog; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(int prev_count = -1); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 7bc6fc6b..8de5211c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -117,7 +117,7 @@ void AtlPage::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion)); auto editedLogoName = selected.safeName; auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 7e90af47..b65ace6b 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -119,29 +119,6 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) } current = listModel->data(first, Qt::UserRole).value<Flame::IndexedPack>(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor& author) { - if (author.url.isEmpty()) { - return author.name; - } - return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "<br>" + tr(" by ") + authorStrs.join(", "); - } - text += "<br><br>"; - - ui->packDescription->setHtml(text + current.description); if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; @@ -188,6 +165,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); } + + updateUi(); } void FlamePage::suggestCurrent() @@ -217,3 +196,46 @@ void FlamePage::onVersionSelectionChanged(QString data) selectedVersion = ui->versionSelectionBox->currentData().toString(); suggestCurrent(); } + +void FlamePage::updateUi() +{ + QString text = ""; + QString name = current.name; + + if (current.extra.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.extra.websiteUrl + "\">" + name + "</a>"; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor& author) { + if (author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + + if(current.extraInfoLoaded) { + if (!current.extra.issuesUrl.isEmpty() + || !current.extra.sourceUrl.isEmpty() + || !current.extra.wikiUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current.extra.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>"; + if (!current.extra.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>"; + if (!current.extra.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>"; + } + + text += "<hr>"; + + ui->packDescription->setHtml(text + current.description); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index baac57c9..8130e416 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -79,6 +79,8 @@ public: virtual bool shouldDisplay() const override; void retranslate() override; + void updateUi(); + void openedImpl() override; bool eventFilter(QObject * watched, QEvent * event) override; diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index 37244fed..ad15b6e6 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -122,10 +122,10 @@ void ListModel::requestFinished() jobPtr.reset(); remainingPacks.clear(); - QJsonParseError parse_error; + QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -169,7 +169,7 @@ void ListModel::packRequestFinished() QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -184,7 +184,7 @@ void ListModel::packRequestFinished() catch (const JSONValidationError &e) { qDebug() << QString::fromUtf8(response); - qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause(); return; } @@ -192,7 +192,7 @@ void ListModel::packRequestFinished() // ignore those "dud" packs. if (pack.versions.empty()) { - qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions"; + qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions"; } else { @@ -270,7 +270,7 @@ void ListModel::requestLogo(QString logo, QString url) bool stale = entry->isStale(); - NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo), APPLICATION->network()); + NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 63b944c4..06e9db4f 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ListModel.h" #include "Application.h" @@ -11,6 +46,8 @@ #include <BuildConfig.h> +#include <net/NetJob.h> + namespace LegacyFTB { FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp index 1d9f4d60..af92e63e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp @@ -30,6 +30,11 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) Modrinth::loadIndexedPack(m, obj); } +void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + Modrinth::loadExtraPackData(m, obj); +} + void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index ae7b0bdd..386897fd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -31,6 +31,7 @@ class ListModel : public ModPlatform::ListModel { private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 7cacf37a..07d1687c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 14aa6747..1b4d8da4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -39,6 +39,7 @@ #include "modplatform/modrinth/ModrinthPackManifest.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "net/NetJob.h" class ModPage; class Version; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 9bd24b57..d8500674 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -224,7 +224,37 @@ void ModrinthPage::updateUI() // TODO: Implement multiple authors with links text += "<br>" + tr(" by ") + QString("<a href=%1>%2</a>").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); - text += "<br>"; + if(current.extraInfoLoaded) { + if (!current.extra.donate.isEmpty()) { + text += "<br><br>" + tr("Donate information: "); + auto donateToStr = [](Modrinth::DonationData& donate) -> QString { + return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current.extra.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + if (!current.extra.issuesUrl.isEmpty() + || !current.extra.sourceUrl.isEmpty() + || !current.extra.wikiUrl.isEmpty() + || !current.extra.discordUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current.extra.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>"; + if (!current.extra.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>"; + if (!current.extra.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>"; + if (!current.extra.discordUrl.isEmpty()) + text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extra.discordUrl) + "<br>"; + } + + text += "<hr>"; HoeDown h; text += h.process(current.extra.body.toUtf8()); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index ca6a9b7e..15bf645f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -60,7 +60,11 @@ </widget> </item> <item row="0" column="1"> - <widget class="QTextBrowser" name="packDescription"/> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> </item> </layout> </item> diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp index 8c4bd690..7d78006b 100644 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ b/launcher/ui/widgets/MCModInfoFrame.cpp @@ -32,7 +32,7 @@ void MCModInfoFrame::updateWithMod(Mod &m) QString text = ""; QString name = ""; if (m.name().isEmpty()) - name = m.mmc_id(); + name = m.internal_id(); else name = m.name(); diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index 265fa66a..6bf671be 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -24,24 +24,65 @@ import java.net.URL; import java.util.Map; import java.util.TreeMap; +/* + * WARNING: This class is reflectively accessed by legacy Forge versions. + * Changing field and method declarations without further testing is not recommended. + */ public final class Launcher extends Applet implements AppletStub { private final Map<String, String> params = new TreeMap<>(); - private final Applet wrappedApplet; + private Applet wrappedApplet; + + private URL documentBase; private boolean active = false; public Launcher(Applet applet) { + this(applet, null); + } + + public Launcher(Applet applet, URL documentBase) { this.setLayout(new BorderLayout()); this.add(applet, "Center"); this.wrappedApplet = applet; + + try { + if (documentBase != null) { + this.documentBase = documentBase; + } else if (applet.getClass().getPackage().getName().startsWith("com.mojang")) { + // Special case only for Classic versions + + this.documentBase = new URL("http", "www.minecraft.net", 80, "/game/"); + } else { + this.documentBase = new URL("http://www.minecraft.net/game/"); + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public void replace(Applet applet) { + this.wrappedApplet = applet; + + applet.setStub(this); + applet.setSize(getWidth(), getHeight()); + + this.setLayout(new BorderLayout()); + this.add(applet, "Center"); + + applet.init(); + + active = true; + + applet.start(); + + validate(); } - public void setParameter(String name, String value) - { + public void setParameter(String name, String value) { params.put(name, value); } @@ -54,7 +95,7 @@ public final class Launcher extends Applet implements AppletStub { try { return super.getParameter(name); - } catch (Exception ignore) {} + } catch (Exception ignored) {} return null; } @@ -108,25 +149,13 @@ public final class Launcher extends Applet implements AppletStub { try { return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { - e.printStackTrace(); + throw new RuntimeException(e); } - - return null; } @Override public URL getDocumentBase() { - try { - // Special case only for Classic versions - if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) - return new URL("http", "www.minecraft.net", 80, "/game/"); - - return new URL("http://www.minecraft.net/game/"); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - - return null; + return documentBase; } @Override diff --git a/nix/default.nix b/nix/default.nix index 969b455e..d6aa370c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -14,6 +14,7 @@ , quazip , libGL , msaClientID ? "" +, extraJDKs ? [ ] # flake , self @@ -36,6 +37,8 @@ let # This variable will be passed to Minecraft by PolyMC gameLibraryPath = libpath + ":/run/opengl-driver/lib"; + + javaPaths = lib.makeSearchPath "bin/java" ([ jdk jdk8 ] ++ extraJDKs); in stdenv.mkDerivation rec { @@ -67,7 +70,7 @@ stdenv.mkDerivation rec { # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 wrapQtApp $out/bin/polymc \ --set GAME_LIBRARY_PATH ${gameLibraryPath} \ - --prefix POLYMC_JAVA_PATHS : ${jdk}/lib/openjdk/bin/java:${jdk8}/lib/openjdk/bin/java \ + --prefix POLYMC_JAVA_PATHS : ${javaPaths} \ --prefix PATH : ${lib.makeBinPath [ xorg.xrandr ]} ''; diff --git a/program_info/org.polymc.PolyMC.desktop.in b/program_info/org.polymc.PolyMC.desktop.in index 2d9e7103..e6d88909 100644 --- a/program_info/org.polymc.PolyMC.desktop.in +++ b/program_info/org.polymc.PolyMC.desktop.in @@ -8,5 +8,5 @@ Exec=@Launcher_APP_BINARY_NAME@ StartupNotify=true Icon=org.polymc.PolyMC Categories=Game; -Keywords=game;minecraft;launcher; +Keywords=game;minecraft;launcher;mc; StartupWMClass=PolyMC |