From e35f2b6c2c4b1c7675a4d7b72c01adbff1a43205 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 24 Dec 2021 15:00:36 +0000 Subject: [PATCH 1/9] NOISSUE Allow Technic pack API urls to be used in search This mimics the behaviour that the Technic launcher has, and their website displays API URLs for. The big benefit of this, is to be able to install private packs now :) --- .../modplatform/technic/TechnicModel.cpp | 80 ++++++++++++++----- .../pages/modplatform/technic/TechnicModel.h | 5 ++ 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 0bc0d498..c62d36a7 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -1,4 +1,5 @@ /* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,12 +96,21 @@ void Technic::ListModel::performSearch() QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { searchUrl = "https://api.technicpack.net/trending?build=multimc"; + searchMode = List; } - else - { + else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=multimc").arg(currentSearchTerm.mid(7)); + searchMode = Single; + } + else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + searchUrl = QString("%1?build=multimc").arg(currentSearchTerm); + searchMode = Single; + } + else { searchUrl = QString( "https://api.technicpack.net/search?build=multimc&q=%1" ).arg(currentSearchTerm); + searchMode = List; } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; @@ -125,26 +135,58 @@ void Technic::ListModel::searchRequestFinished() QList newList; try { auto root = Json::requireObject(doc); - auto objs = Json::requireArray(root, "modpacks"); - for (auto technicPack: objs) { - Modpack pack; - auto technicPackObject = Json::requireValueObject(technicPack); - pack.name = Json::requireString(technicPackObject, "name"); - pack.slug = Json::requireString(technicPackObject, "slug"); - if (pack.slug == "vanilla") - continue; - auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); - if(rawURL == "null") { - pack.logoUrl = "null"; - pack.logoName = "null"; + switch (searchMode) { + case List: { + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireValueObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + break; } - else { - pack.logoUrl = rawURL; - pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + case Single: { + if (root.contains("error")) { + // Invalid API url + break; + } + + Modpack pack; + pack.name = Json::requireString(root, "displayName"); + pack.slug = Json::requireString(root, "name"); + + if (root.contains("icon")) { + auto iconObj = Json::requireObject(root, "icon"); + auto iconUrl = Json::requireString(iconObj, "url"); + + pack.logoUrl = iconUrl; + pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + + pack.broken = false; + newList.append(pack); + break; } - pack.broken = false; - newList.append(pack); } } catch (const JSONValidationError &err) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index e80e6e7c..d2748811 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -1,4 +1,5 @@ /* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +64,10 @@ private: ResetRequested, Finished } searchState = None; + enum SearchMode { + List, + Single, + } searchMode = List; NetJob::Ptr jobPtr; QByteArray response; }; From ddc094b76ba60ff2f1f158317f0bf31bbd7a420d Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 24 Dec 2021 15:20:34 +0000 Subject: [PATCH 2/9] NOISUE Prevent potential HTML injection --- launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 64054168..09535017 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -178,14 +178,12 @@ void TechnicPage::metadataLoaded() QString name = current.name; if (current.websiteUrl.isEmpty()) - // This allows injecting HTML here. - text = name; + text = name.toHtmlEscaped(); else - // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML. - text = "" + name + ""; + text = "" + name.toHtmlEscaped() + ""; + if (!current.author.isEmpty()) { - // This allows injecting HTML here - text += tr(" by ") + current.author; + text += tr(" by ") + current.author.toHtmlEscaped(); } text += "

"; From 65a4f8919ab0f5e3d97db264a9a455cdf0df55fe Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 24 Dec 2021 15:10:42 +0000 Subject: [PATCH 3/9] NOISSUE Include the modpack version in instance title --- launcher/ui/pages/modplatform/technic/TechnicData.h | 1 + launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 50fd75e8..4a374a6b 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -36,6 +36,7 @@ struct Modpack { QString websiteUrl; QString author; QString description; + QString currentVersion; }; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 09535017..34d18052 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -165,6 +165,7 @@ void TechnicPage::suggestCurrent() current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); current.metadataLoaded = true; metadataLoaded(); }); @@ -191,11 +192,11 @@ void TechnicPage::metadataLoaded() ui->packDescription->setHtml(text + current.description); if (!current.isSolder) { - dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { while (current.url.endsWith('/')) current.url.chop(1); - dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); } } From 2334a44221ac19bd90acd9c23c909b37899a3fd5 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 27 Dec 2021 03:55:08 +0000 Subject: [PATCH 4/9] NOISSUE Match CurseForge pack description format in Technic UI --- launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 34d18052..59d78bfc 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -184,7 +184,7 @@ void TechnicPage::metadataLoaded() text = "" + name.toHtmlEscaped() + ""; if (!current.author.isEmpty()) { - text += tr(" by ") + current.author.toHtmlEscaped(); + text += "
" + tr(" by ") + current.author.toHtmlEscaped(); } text += "

"; From 18f790953aa9e2beb6aa6f1127c409ab039c3829 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 1 Apr 2022 23:40:20 +0100 Subject: [PATCH 5/9] NOISSUE Add API models for Solder packs This will be able to replace the ugly parsing/usage code that currently exists in the Solder install task :) --- launcher/CMakeLists.txt | 2 + .../technic/SolderPackManifest.cpp | 56 +++++++++++++++++++ .../modplatform/technic/SolderPackManifest.h | 47 ++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 launcher/modplatform/technic/SolderPackManifest.cpp create mode 100644 launcher/modplatform/technic/SolderPackManifest.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 25c28063..85f669eb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -509,6 +509,8 @@ set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.cpp modplatform/technic/SolderPackInstallTask.h modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/SolderPackManifest.h + modplatform/technic/SolderPackManifest.cpp modplatform/technic/TechnicPackProcessor.h modplatform/technic/TechnicPackProcessor.cpp ) diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp new file mode 100644 index 00000000..ad195d3a --- /dev/null +++ b/launcher/modplatform/technic/SolderPackManifest.cpp @@ -0,0 +1,56 @@ +/* + * Copyright 2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SolderPackManifest.h" + +#include "Json.h" + +namespace TechnicSolder { + +void loadPack(Pack& v, QJsonObject& obj) +{ + v.recommended = Json::requireString(obj, "recommended"); + v.latest = Json::requireString(obj, "latest"); + + auto builds = Json::requireArray(obj, "builds"); + for (const auto buildRaw : builds) { + auto build = Json::requireValueString(buildRaw); + v.builds.append(build); + } +} + +static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) +{ + b.name = Json::requireString(obj, "name"); + b.version = Json::requireString(obj, "version"); + b.md5 = Json::requireString(obj, "md5"); + b.url = Json::requireString(obj, "url"); +} + +void loadPackBuild(PackBuild& v, QJsonObject& obj) +{ + v.minecraft = Json::requireString(obj, "minecraft"); + + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) { + auto modObj = Json::requireValueObject(modRaw); + PackBuildMod mod; + loadPackBuildMod(mod, modObj); + v.mods.append(mod); + } +} + +} diff --git a/launcher/modplatform/technic/SolderPackManifest.h b/launcher/modplatform/technic/SolderPackManifest.h new file mode 100644 index 00000000..b4d8cf53 --- /dev/null +++ b/launcher/modplatform/technic/SolderPackManifest.h @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace TechnicSolder { + +struct Pack { + QString recommended; + QString latest; + QVector builds; +}; + +void loadPack(Pack& v, QJsonObject& obj); + +struct PackBuildMod { + QString name; + QString version; + QString md5; + QString url; +}; + +struct PackBuild { + QString minecraft; + QVector mods; +}; + +void loadPackBuild(PackBuild& v, QJsonObject& obj); + +} From b8a736c67316040518dd9e2c93028c4c1303cc25 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 2 Apr 2022 13:26:03 +0100 Subject: [PATCH 6/9] NOISSUE Replace inline parsing code with Solder API models --- .../technic/SolderPackInstallTask.cpp | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 4e9c7ad3..673f93e0 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +21,7 @@ #include #include #include "TechnicPackProcessor.h" +#include "SolderPackManifest.h" Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, @@ -52,21 +54,29 @@ void Technic::SolderPackInstallTask::executeTask() void Technic::SolderPackInstallTask::versionSucceeded() { - try - { - QJsonDocument doc = Json::requireDocument(m_response); - QJsonObject obj = Json::requireObject(doc); - QString version = Json::requireString(obj, "recommended", "__placeholder__"); - m_sourceUrl = m_sourceUrl.toString() + '/' + version; + setStatus(tr("Resolving modpack files")); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; } - catch (const JSONValidationError &e) - { - emitFailed(e.cause()); + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); m_filesNetJob.reset(); return; } - setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString())); + m_sourceUrl = m_sourceUrl.toString() + '/' + pack.recommended; + m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); auto job = m_filesNetJob.get(); @@ -77,38 +87,40 @@ void Technic::SolderPackInstallTask::versionSucceeded() void Technic::SolderPackInstallTask::fileListSucceeded() { - setStatus(tr("Downloading modpack:")); - QStringList modUrls; - try - { - QJsonDocument doc = Json::requireDocument(m_response); - QJsonObject obj = Json::requireObject(doc); - QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); - if (!minecraftVersion.isEmpty()) - m_minecraftVersion = minecraftVersion; - QJsonArray mods = Json::requireArray(obj, "mods", "'mods'"); - for (auto mod: mods) - { - QJsonObject modObject = Json::requireValueObject(mod); - modUrls.append(Json::requireString(modObject, "url", "'url'")); - } + setStatus(tr("Downloading modpack")); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; } - catch (const JSONValidationError &e) - { - emitFailed(e.cause()); + auto obj = doc.object(); + + TechnicSolder::PackBuild build; + try { + TechnicSolder::loadPackBuild(build, obj); + } + catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); m_filesNetJob.reset(); return; } + + if (!build.minecraft.isEmpty()) + m_minecraftVersion = build.minecraft; + m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); int i = 0; - for (auto &modUrl: modUrls) + for (const auto &mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path)); + m_filesNetJob->addNetAction(Net::Download::makeFile(mod.url, path)); i++; } - m_modCount = modUrls.size(); + m_modCount = build.mods.size(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); @@ -206,6 +218,4 @@ void Technic::SolderPackInstallTask::extractFinished() void Technic::SolderPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); - return; } - From b6290ac254f5d5e5cac704aedbdf2e00f1c28f43 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 27 Dec 2021 03:52:54 +0000 Subject: [PATCH 7/9] GH-3516 Display available versions for Solder packs --- .../technic/SolderPackInstallTask.cpp | 40 +----- .../technic/SolderPackInstallTask.h | 5 +- .../pages/modplatform/technic/TechnicData.h | 6 + .../pages/modplatform/technic/TechnicPage.cpp | 125 ++++++++++++++++-- .../pages/modplatform/technic/TechnicPage.h | 10 ++ .../pages/modplatform/technic/TechnicPage.ui | 97 +++++++------- 6 files changed, 191 insertions(+), 92 deletions(-) diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 673f93e0..d2babf84 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -1,5 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors - * Copyright 2022 Jamie Mansfield + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +26,12 @@ Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, const QUrl &sourceUrl, + const QString &version, const QString &minecraftVersion ) { m_sourceUrl = sourceUrl; m_minecraftVersion = minecraftVersion; + m_version = version; m_network = network; } @@ -42,43 +44,13 @@ bool Technic::SolderPackInstallTask::abort() { } void Technic::SolderPackInstallTask::executeTask() -{ - setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); - m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); - auto job = m_filesNetJob.get(); - connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded); - connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); - m_filesNetJob->start(); -} - -void Technic::SolderPackInstallTask::versionSucceeded() { setStatus(tr("Resolving modpack files")); - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << m_response; - return; - } - auto obj = doc.object(); - - TechnicSolder::Pack pack; - try { - TechnicSolder::loadPack(pack, obj); - } - catch (const JSONValidationError& e) { - emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); - m_filesNetJob.reset(); - return; - } - - m_sourceUrl = m_sourceUrl.toString() + '/' + pack.recommended; - m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto sourceUrl = QString("%1/%2").arg(m_sourceUrl.toString(), m_version); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index 9b2058d8..21596783 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +28,7 @@ namespace Technic { Q_OBJECT public: - explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &sourceUrl, const QString &minecraftVersion); + explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &sourceUrl, const QString& version, const QString &minecraftVersion); bool canAbort() const override { return true; } bool abort() override; @@ -37,7 +38,6 @@ namespace Technic virtual void executeTask() override; private slots: - void versionSucceeded(); void fileListSucceeded(); void downloadSucceeded(); void downloadFailed(QString reason); @@ -52,6 +52,7 @@ namespace Technic NetJob::Ptr m_filesNetJob; QUrl m_sourceUrl; + QString m_version; QString m_minecraftVersion; QByteArray m_response; QTemporaryDir m_outputDir; diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 4a374a6b..e92cda17 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -1,4 +1,5 @@ /* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ #include #include +#include namespace Technic { struct Modpack { @@ -37,6 +39,10 @@ struct Modpack { QString author; QString description; QString currentVersion; + + bool versionsLoaded = false; + QString recommended; + QVector versions; }; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 59d78bfc..bfeb3690 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -1,4 +1,5 @@ /* Copyright 2013-2022 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +25,7 @@ #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" +#include "modplatform/technic/SolderPackManifest.h" #include "Json.h" #include "Application.h" @@ -36,7 +38,9 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -74,13 +78,14 @@ void TechnicPage::triggerSearch() { void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) { + ui->versionSelectionBox->clear(); + if(!first.isValid()) { if(isOpened) { dialog->setSuggestedPack(); } - //ui->frame->clear(); return; } @@ -113,17 +118,19 @@ void TechnicPage::suggestCurrent() } NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr response = std::make_shared(); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), &response)); + QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] { + jobPtr.reset(); + if (current.slug != slug) { return; } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonObject obj = doc.object(); if(parse_error.error != QJsonParseError::NoError) { @@ -167,9 +174,12 @@ void TechnicPage::suggestCurrent() current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); current.metadataLoaded = true; + metadataLoaded(); }); - netJob->start(); + + jobPtr = netJob; + jobPtr->start(); } // expects current.metadataLoaded to be true @@ -190,13 +200,108 @@ void TechnicPage::metadataLoaded() text += "

"; ui->packDescription->setHtml(text + current.description); + + // Strip trailing forward-slashes from Solder URL's + if (current.isSolder) { + while (current.url.endsWith('/')) current.url.chop(1); + } + + // Display versions from Solder + if (!current.isSolder) { + // If the pack isn't a Solder pack, it only has the single version + ui->versionSelectionBox->addItem(current.currentVersion); + } + else if (current.versionsLoaded) { + // reverse foreach, so that the newest versions are first + for (auto i = current.versions.size(); i--;) { + ui->versionSelectionBox->addItem(current.versions.at(i)); + } + ui->versionSelectionBox->setCurrentText(current.recommended); + } + else { + // For now, until the versions are pulled from the Solder instance, display the current + // version so we can display something quicker + ui->versionSelectionBox->addItem(current.currentVersion); + + auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto url = QString("%1/modpack/%2").arg(current.url, current.slug); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + + jobPtr = netJob; + jobPtr->start(); + } + + selectVersion(); +} + +void TechnicPage::selectVersion() { + if (!isOpened) { + return; + } + if (current.broken) { + dialog->setSuggestedPack(); + return; + } + if (!current.isSolder) { - dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { - while (current.url.endsWith('/')) current.url.chop(1); - dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, selectedVersion, current.minecraftVersion)); } } + +void TechnicPage::onSolderLoaded() { + jobPtr.reset(); + + auto fallback = [this]() { + current.versionsLoaded = true; + + current.versions.clear(); + current.versions.append(current.currentVersion); + }; + + current.versions.clear(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + fallback(); + return; + } + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError &err) { + qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); + fallback(); + return; + } + + current.versionsLoaded = true; + current.recommended = pack.recommended; + current.versions.append(pack.builds); + + // Finally, let's reload :) + ui->versionSelectionBox->clear(); + metadataLoaded(); +} + +void TechnicPage::onVersionSelectionChanged(QString data) { + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; + } + + selectedVersion = data; + selectVersion(); +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 21695dd0..920ca009 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -1,4 +1,5 @@ /* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +20,7 @@ #include "ui/pages/BasePage.h" #include +#include "net/NetJob.h" #include "tasks/Task.h" #include "TechnicData.h" @@ -65,14 +67,22 @@ public: private: void suggestCurrent(); void metadataLoaded(); + void selectVersion(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onSolderLoaded(); + void onVersionSelectionChanged(QString data); private: Ui::TechnicPage *ui = nullptr; NewInstanceDialog* dialog = nullptr; Technic::ListModel* model = nullptr; + Technic::Modpack current; + QString selectedVersion; + + NetJob::Ptr jobPtr; + QByteArray response; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index ab6b4255..aa2df370 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -10,52 +10,44 @@ 405 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Search and filter ... - - - - - - - Search - - - - - - - - - - 0 - + + + + + + - + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - - Qt::ScrollBarAlwaysOff + + + Qt::Horizontal + + QSizePolicy::Preferred + + + + 1 + 1 + + + + + + + + + + true @@ -67,14 +59,27 @@ + + + + + + + Search and filter ... + + + + + + + Search + + + - - searchEdit - searchButton - From fb0970b4963b8d0aacaa92f5c372f4c3ac3be0d6 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 2 Apr 2022 00:11:33 +0100 Subject: [PATCH 8/9] NOISSUE Verify checksums for pack build mods --- launcher/modplatform/technic/SolderPackInstallTask.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index d2babf84..ccd0b921 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -22,6 +22,7 @@ #include #include "TechnicPackProcessor.h" #include "SolderPackManifest.h" +#include "net/ChecksumValidator.h" Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, @@ -88,7 +89,14 @@ void Technic::SolderPackInstallTask::fileListSucceeded() for (const auto &mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - m_filesNetJob->addNetAction(Net::Download::makeFile(mod.url, path)); + + auto dl = Net::Download::makeFile(mod.url, path); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + m_filesNetJob->addNetAction(dl); + i++; } From 9bdfa5c8de31879676578a137659650f27156126 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 9 Jan 2022 23:02:32 +0000 Subject: [PATCH 9/9] NOISSUE Make Technic API base URL and build constants --- buildconfig/BuildConfig.h | 6 ++++++ .../ui/pages/modplatform/technic/TechnicModel.cpp | 13 ++++++++----- .../ui/pages/modplatform/technic/TechnicPage.cpp | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 903827ea..7ab5b100 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -100,6 +100,12 @@ public: QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; + /** + * The build that is reported to the Technic API. + */ + QString TECHNIC_API_BUILD = "multimc"; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index c62d36a7..b0128b98 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -16,6 +16,7 @@ #include "TechnicModel.h" #include "Application.h" +#include "BuildConfig.h" #include "Json.h" #include @@ -95,21 +96,23 @@ void Technic::ListModel::performSearch() NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = "https://api.technicpack.net/trending?build=multimc"; + searchUrl = QString("%1trending?build=%2") + .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); searchMode = List; } else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { - searchUrl = QString("https://%1?build=multimc").arg(currentSearchTerm.mid(7)); + searchUrl = QString("https://%1?build=%2") + .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { - searchUrl = QString("%1?build=multimc").arg(currentSearchTerm); + searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else { searchUrl = QString( - "https://api.technicpack.net/search?build=multimc&q=%1" - ).arg(currentSearchTerm); + "%1search?build=%2&q=%3" + ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index bfeb3690..bfbe1639 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -22,6 +22,7 @@ #include "ui/dialogs/NewInstanceDialog.h" +#include "BuildConfig.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" @@ -119,7 +120,7 @@ void TechnicPage::suggestCurrent() NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] { jobPtr.reset();