From 8021fb25d0778bbc88bbfa052fcf2572807ee0f5 Mon Sep 17 00:00:00 2001 From: kb1000 Date: Sun, 7 Jun 2020 17:46:12 +0200 Subject: [PATCH] GH-469 Implement support for importing and searching for Technic Platform and Solder modpacks This does not support any custom modpack.jar for 1.6 or newer, it simply uses standard Forge then. Supports Forge and Fabric, and JAR mods for 1.5 and older. --- api/logic/CMakeLists.txt | 10 + api/logic/Env.cpp | 1 + api/logic/InstanceImportTask.cpp | 40 +++- api/logic/InstanceImportTask.h | 19 +- api/logic/MMCZip.cpp | 2 +- api/logic/MMCZip.h | 3 +- .../technic/SingleZipPackInstallTask.cpp | 129 ++++++++++ .../technic/SingleZipPackInstallTask.h | 64 +++++ .../technic/SolderPackInstallTask.cpp | 194 +++++++++++++++ .../technic/SolderPackInstallTask.h | 57 +++++ .../technic/TechnicPackProcessor.cpp | 201 ++++++++++++++++ .../technic/TechnicPackProcessor.h | 37 +++ api/logic/net/NetJob.cpp | 2 + api/logic/net/NetJob.h | 2 +- application/CMakeLists.txt | 5 + application/dialogs/NewInstanceDialog.cpp | 6 +- .../pages/modplatform/technic/TechnicData.h | 40 ++++ .../modplatform/technic/TechnicModel.cpp | 223 ++++++++++++++++++ .../pages/modplatform/technic/TechnicModel.h | 70 ++++++ .../pages/modplatform/technic/TechnicPage.cpp | 204 ++++++++++++++++ .../pages/modplatform/technic/TechnicPage.h | 78 ++++++ .../pages/modplatform/technic/TechnicPage.ui | 62 +++++ .../pages/modplatform/twitch/TwitchModel.cpp | 2 +- .../resources/assets/underconstruction.png | Bin 0 -> 14490 bytes 24 files changed, 1441 insertions(+), 10 deletions(-) create mode 100644 api/logic/modplatform/technic/SingleZipPackInstallTask.cpp create mode 100644 api/logic/modplatform/technic/SingleZipPackInstallTask.h create mode 100644 api/logic/modplatform/technic/SolderPackInstallTask.cpp create mode 100644 api/logic/modplatform/technic/SolderPackInstallTask.h create mode 100644 api/logic/modplatform/technic/TechnicPackProcessor.cpp create mode 100644 api/logic/modplatform/technic/TechnicPackProcessor.h create mode 100644 application/pages/modplatform/technic/TechnicData.h create mode 100644 application/pages/modplatform/technic/TechnicModel.cpp create mode 100644 application/pages/modplatform/technic/TechnicModel.h create mode 100644 application/pages/modplatform/technic/TechnicPage.cpp create mode 100644 application/pages/modplatform/technic/TechnicPage.h create mode 100644 application/pages/modplatform/technic/TechnicPage.ui create mode 100644 application/resources/assets/underconstruction.png diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 6e9aec083..15916bb58 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -477,6 +477,15 @@ set(MODPACKSCH_SOURCES modplatform/modpacksch/FTBPackManifest.cpp ) +set(TECHNIC_SOURCES + modplatform/technic/SingleZipPackInstallTask.h + modplatform/technic/SingleZipPackInstallTask.cpp + modplatform/technic/SolderPackInstallTask.h + modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/TechnicPackProcessor.h + modplatform/technic/TechnicPackProcessor.cpp +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS MultiMC_logic @@ -508,6 +517,7 @@ set(LOGIC_SOURCES ${FTB_SOURCES} ${FLAME_SOURCES} ${MODPACKSCH_SOURCES} + ${TECHNIC_SOURCES} ) add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp index 2043f9826..e9eb67cbd 100644 --- a/api/logic/Env.cpp +++ b/api/logic/Env.cpp @@ -98,6 +98,7 @@ void Env::initHttpMetaCache() m_metacache->addBase("general", QDir("cache").absolutePath()); m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); + m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath()); m_metacache->addBase("skins", QDir("accounts/skins").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index e21874160..772149c41 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -1,3 +1,18 @@ +/* Copyright 2013-2020 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 "InstanceImportTask.h" #include "BaseInstance.h" #include "FileSystem.h" @@ -15,6 +30,8 @@ #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" #include "Json.h" +#include +#include "modplatform/technic/TechnicPackProcessor.h" InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { @@ -23,8 +40,6 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) void InstanceImportTask::executeTask() { - InstancePtr newInstance; - if (m_sourceUrl.isLocalFile()) { m_archivePath = m_sourceUrl.toLocalFile(); @@ -82,6 +97,7 @@ void InstanceImportTask::processZipPack() QStringList blacklist = {"instance.cfg", "manifest.json"}; QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); + bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); QString root; if(!mmcFound.isNull()) @@ -91,6 +107,14 @@ void InstanceImportTask::processZipPack() root = mmcFound; m_modpackType = ModpackType::MultiMC; } + else if (technicFound) + { + // process as Technic pack + qDebug() << "Technic:" << technicFound; + extractDir.mkpath(".minecraft"); + extractDir.cd(".minecraft"); + m_modpackType = ModpackType::Technic; + } else if(!flameFound.isNull()) { // process as Flame pack @@ -98,7 +122,6 @@ void InstanceImportTask::processZipPack() root = flameFound; m_modpackType = ModpackType::Flame; } - if(m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -161,6 +184,9 @@ void InstanceImportTask::extractFinished() case ModpackType::MultiMC: processMultiMC(); return; + case ModpackType::Technic: + processTechnic(); + return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; @@ -371,6 +397,14 @@ void InstanceImportTask::processFlame() m_modIdResolver->start(); } +void InstanceImportTask::processTechnic() +{ + shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath); +} + void InstanceImportTask::processMultiMC() { // FIXME: copy from FolderInstanceProvider!!! FIX IT!!! diff --git a/api/logic/InstanceImportTask.h b/api/logic/InstanceImportTask.h index d326391b9..1e19354b6 100644 --- a/api/logic/InstanceImportTask.h +++ b/api/logic/InstanceImportTask.h @@ -1,3 +1,18 @@ +/* Copyright 2013-2020 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 "InstanceTask.h" @@ -29,6 +44,7 @@ private: void processZipPack(); void processMultiMC(); void processFlame(); + void processTechnic(); private slots: void downloadSucceeded(); @@ -49,6 +65,7 @@ private: /* data */ enum class ModpackType{ Unknown, MultiMC, - Flame + Flame, + Technic } m_modpackType = ModpackType::Unknown; }; diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp index 3afdbf5e7..876d73287 100644 --- a/api/logic/MMCZip.cpp +++ b/api/logic/MMCZip.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2019 MultiMC Contributors +/* Copyright 2013-2020 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index 85ac7802c..56d20fbe7 100644 --- a/api/logic/MMCZip.h +++ b/api/logic/MMCZip.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2019 MultiMC Contributors +/* Copyright 2013-2020 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,5 +67,4 @@ namespace MMCZip * \return The list of the full paths of the files extracted, empty on failure. */ QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); - } diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp new file mode 100644 index 000000000..833ac0a22 --- /dev/null +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp @@ -0,0 +1,129 @@ +/* Copyright 2013-2020 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 "SingleZipPackInstallTask.h" + +#include "Env.h" +#include "MMCZip.h" +#include "TechnicPackProcessor.h" + +#include + +Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion) +{ + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; +} + +void Technic::SingleZipPackInstallTask::executeTask() +{ + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = ENV.metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob.reset(new NetJob(tr("Modpack download"))); + m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); + connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); + connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SingleZipPackInstallTask::downloadSucceeded() +{ + setStatus(tr("Extracting modpack")); + QDir extractDir(m_stagingPath); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + m_packZip.reset(new QuaZip(m_archivePath)); + if (!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) +{ + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + setProgress(current / 2, total); +} + +void Technic::SingleZipPackInstallTask::extractFinished() +{ + m_packZip.reset(); + if (m_extractFuture.result().isEmpty()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if (file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if (origPermissions != permissions) + { + if (!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion); +} + +void Technic::SingleZipPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); +} diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.h b/api/logic/modplatform/technic/SingleZipPackInstallTask.h new file mode 100644 index 000000000..929476bb2 --- /dev/null +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.h @@ -0,0 +1,64 @@ +/* Copyright 2013-2020 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 + +#ifndef TECHNIC_SINGLEZIPPACKINSTALLTASK_H +#define TECHNIC_SINGLEZIPPACKINSTALLTASK_H + +#include "InstanceTask.h" +#include "net/NetJob.h" +#include "multimc_logic_export.h" + +#include "quazip.h" + +#include +#include +#include + +namespace Technic { + +class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + +protected: + void executeTask() override; + + +private slots: + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + +private: + QUrl m_sourceUrl; + QString m_minecraftVersion; + QString m_archivePath; + NetJobPtr m_filesNetJob; + std::unique_ptr m_packZip; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; +}; + +} // namespace Technic + +#endif // TECHNIC_SINGLEZIPPACKINSTALLTASK_H diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.cpp b/api/logic/modplatform/technic/SolderPackInstallTask.cpp new file mode 100644 index 000000000..cb440e843 --- /dev/null +++ b/api/logic/modplatform/technic/SolderPackInstallTask.cpp @@ -0,0 +1,194 @@ +/* Copyright 2013-2020 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 "SolderPackInstallTask.h" + +#include +#include +#include +#include +#include "TechnicPackProcessor.h" + +Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion) +{ + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; +} + +void Technic::SolderPackInstallTask::executeTask() +{ + setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); + m_filesNetJob.reset(new NetJob(tr("Finding recommended version"))); + 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() +{ + try + { + QJsonDocument doc = Json::requireDocument(m_response); + QJsonObject obj = Json::requireObject(doc); + QString version = Json::requireString(obj, "recommended", "__placeholder__"); + m_sourceUrl = m_sourceUrl.toString() + '/' + version; + } + catch (const JSONValidationError &e) + { + emitFailed(e.cause()); + m_filesNetJob.reset(); + return; + } + + setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString())); + m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"))); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); + connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +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::requireObject(mod); + modUrls.append(Json::requireString(modObject, "url", "'url'")); + } + } + catch (const JSONValidationError &e) + { + emitFailed(e.cause()); + m_filesNetJob.reset(); + return; + } + m_filesNetJob.reset(new NetJob(tr("Downloading modpack"))); + int i = 0; + for (auto &modUrl: modUrls) + { + m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, m_outputDir.filePath(QString("%1").arg(i)))); + i++; + } + + m_modCount = modUrls.size(); + + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::downloadSucceeded() +{ + setStatus(tr("Extracting modpack")); + m_filesNetJob.reset(); + m_extractFuture = QtConcurrent::run([this]() + { + int i = 0; + QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft"); + FS::ensureFolderPathExists(extractDir); + + while (m_modCount > i) + { + if (MMCZip::extractDir(m_outputDir.filePath(QString("%1").arg(i)), extractDir).isEmpty()) + { + return false; + } + i++; + } + return true; + }); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SolderPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SolderPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void Technic::SolderPackInstallTask::downloadFailed(QString reason) +{ + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + setProgress(current / 2, total); +} + +void Technic::SolderPackInstallTask::extractFinished() +{ + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if(file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if(origPermissions != permissions) + { + if(!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true); // TODO: pass the minecraft version down +} + +void Technic::SolderPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); + return; +} + diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.h b/api/logic/modplatform/technic/SolderPackInstallTask.h new file mode 100644 index 000000000..d3a1d0fd1 --- /dev/null +++ b/api/logic/modplatform/technic/SolderPackInstallTask.h @@ -0,0 +1,57 @@ +/* Copyright 2013-2020 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 +#include +#include + +#include + + +namespace Technic +{ + class MULTIMC_LOGIC_EXPORT SolderPackInstallTask : public InstanceTask + { + Q_OBJECT + public: + explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + + private slots: + void versionSucceeded(); + void fileListSucceeded(); + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + + private: + NetJobPtr m_filesNetJob; + QUrl m_sourceUrl; + QString m_minecraftVersion; + QByteArray m_response; + QTemporaryDir m_outputDir; + int m_modCount; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; + }; +} diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.cpp b/api/logic/modplatform/technic/TechnicPackProcessor.cpp new file mode 100644 index 000000000..f986a5295 --- /dev/null +++ b/api/logic/modplatform/technic/TechnicPackProcessor.cpp @@ -0,0 +1,201 @@ +/* Copyright 2020 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 "TechnicPackProcessor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder) +{ + QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); + QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); + + instance.setName(instName); + + if (instIcon != "default") + { + instance.setIconKey(instIcon); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + QByteArray data; + + QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar"); + QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); + QString fmlMinecraftVersion; + if (QFile::exists(modpackJar)) + { + QuaZip zipFile(modpackJar); + if (!zipFile.open(QuaZip::mdUnzip)) + { + emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); + return; + } + QuaZipDir zipFileRoot(&zipFile, "/"); + if (zipFileRoot.exists("/version.json")) + { + if (zipFileRoot.exists("/fmlversion.properties")) + { + zipFile.setCurrentFile("fmlversion.properties"); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"fmlversion.properties\"!")); + return; + } + QByteArray fmlVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(fmlVersionData); + // If not present, this evaluates to a null string + fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); + } + zipFile.setCurrentFile("version.json", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + if (minecraftVersion.isEmpty()) + emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown")); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->installJarMods({modpackJar}); + + // Forge for 1.4.7 and for 1.5.2 require extra libraries. + // Figure out the forge version and add it as a component + // (the code still comes from the jar mod installed above) + if (zipFileRoot.exists("/forgeversion.properties")) + { + zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + // Really shouldn't happen, but error handling shall not be forgotten + emit failed(tr("Unable to open \"forgeversion.properties\"")); + return; + } + QByteArray forgeVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(forgeVersionData); + QString major, minor, revision, build; + major = iniFile["forge.major.number"].toString(); + minor = iniFile["forge.minor.number"].toString(); + revision = iniFile["forge.revision.number"].toString(); + build = iniFile["forge.build.number"].toString(); + + if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty()) + { + emit failed(tr("Invalid \"forgeversion.properties\"!")); + return; + } + + components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build); + } + + components->saveNow(); + emit succeeded(); + return; + } + } + else if (QFile::exists(versionJson)) + { + QFile file(versionJson); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + // This is the "Vanilla" modpack, excluded by the search code + emit failed(tr("Unable to find a \"version.json\"!")); + return; + } + + try + { + QJsonDocument doc = Json::requireDocument(data); + QJsonObject root = Json::requireObject(doc, "version.json"); + QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); + if (minecraftVersion.isEmpty()) + { + if (fmlMinecraftVersion.isEmpty()) + { + emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); + return; + } + minecraftVersion = fmlMinecraftVersion; + } + components->setComponentVersion("net.minecraft", minecraftVersion, true); + for (auto library: Json::ensureArray(root, "libraries", {})) + { + if (!library.isObject()) + { + continue; + } + + auto libraryObject = Json::ensureObject(library, {}, ""); + auto libraryName = Json::ensureString(libraryObject, "name", "", ""); + + if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-')) + { + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); + } + else if (libraryName.startsWith("net.minecraftforge:minecraftforge:")) + { + components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2)); + } + else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) + { + components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2)); + } + } + } + catch (const JSONValidationError &e) + { + emit failed(tr("Could not understand \"version.json\":\n") + e.cause()); + return; + } + + components->saveNow(); + emit succeeded(); +} diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.h b/api/logic/modplatform/technic/TechnicPackProcessor.h new file mode 100644 index 000000000..49d046a5f --- /dev/null +++ b/api/logic/modplatform/technic/TechnicPackProcessor.h @@ -0,0 +1,37 @@ +/* Copyright 2020 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 +#include "settings/SettingsObject.h" + + +namespace Technic +{ + // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask + class TechnicPackProcessor : public QObject + { + Q_OBJECT + + signals: + void succeeded(); + void failed(QString reason); + + public: + void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false); + }; +} diff --git a/api/logic/net/NetJob.cpp b/api/logic/net/NetJob.cpp index 71e317369..7dfa16cae 100644 --- a/api/logic/net/NetJob.cpp +++ b/api/logic/net/NetJob.cpp @@ -214,3 +214,5 @@ bool NetJob::addNetAction(NetActionPtr action) } return true; } + +NetJob::~NetJob() = default; diff --git a/api/logic/net/NetJob.h b/api/logic/net/NetJob.h index 0b56bdaa6..daca419ed 100644 --- a/api/logic/net/NetJob.h +++ b/api/logic/net/NetJob.h @@ -34,7 +34,7 @@ public: { setObjectName(job_name); } - virtual ~NetJob() {} + virtual ~NetJob(); bool addNetAction(NetActionPtr action); diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 802789a28..38bd586b4 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -137,6 +137,10 @@ SET(MULTIMC_SOURCES pages/modplatform/twitch/TwitchModel.h pages/modplatform/twitch/TwitchPage.cpp pages/modplatform/twitch/TwitchPage.h + pages/modplatform/technic/TechnicModel.cpp + pages/modplatform/technic/TechnicModel.h + pages/modplatform/technic/TechnicPage.cpp + pages/modplatform/technic/TechnicPage.h pages/modplatform/ImportPage.cpp pages/modplatform/ImportPage.h @@ -257,6 +261,7 @@ SET(MULTIMC_UIS pages/modplatform/ftb/FtbPage.ui pages/modplatform/legacy_ftb/Page.ui pages/modplatform/twitch/TwitchPage.ui + pages/modplatform/technic/TechnicPage.ui pages/modplatform/ImportPage.ui # Dialogs diff --git a/application/dialogs/NewInstanceDialog.cpp b/application/dialogs/NewInstanceDialog.cpp index d8abdbd4a..c2887b017 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/application/dialogs/NewInstanceDialog.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2019 MultiMC Contributors +/* Copyright 2013-2020 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,8 @@ #include #include #include +#include + NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent) @@ -122,12 +124,14 @@ QList NewInstanceDialog::getPages() { importPage = new ImportPage(this); twitchPage = new TwitchPage(this); + auto technicPage = new TechnicPage(this); return { new VanillaPage(this), importPage, new FtbPage(this), new LegacyFTB::Page(this), + technicPage, twitchPage }; } diff --git a/application/pages/modplatform/technic/TechnicData.h b/application/pages/modplatform/technic/TechnicData.h new file mode 100644 index 000000000..5c7466194 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicData.h @@ -0,0 +1,40 @@ +/* Copyright 2020 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 +#include + + +namespace Technic { +struct Modpack { + QString slug; + + QString name; + QString logoUrl; + QString logoName; + + bool broken = true; + + QString url; + bool isSolder = false; + QString minecraftVersion; + + bool metadataLoaded = false; +}; +} + +Q_DECLARE_METATYPE(Technic::Modpack) diff --git a/application/pages/modplatform/technic/TechnicModel.cpp b/application/pages/modplatform/technic/TechnicModel.cpp new file mode 100644 index 000000000..b3d36bac1 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicModel.cpp @@ -0,0 +1,223 @@ +/* Copyright 2020 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 "TechnicModel.h" +#include "Env.h" +#include "MultiMC.h" + +#include + + +Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +Technic::ListModel::~ListModel() +{ +} + +QVariant Technic::ListModel::data(const QModelIndex& index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = MMC->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + return QVariant(); +} + +int Technic::ListModel::columnCount(const QModelIndex&) const +{ + return 1; +} + +int Technic::ListModel::rowCount(const QModelIndex&) const +{ + return modpacks.size(); +} + +void Technic::ListModel::searchWithTerm(const QString& term) +{ + if(currentSearchTerm == term) { + return; + } + currentSearchTerm = term; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + performSearch(); +} + +void Technic::ListModel::performSearch() +{ + NetJob *netJob = new NetJob("Technic::Search"); + auto searchUrl = QString( + "https://api.technicpack.net/search?build=multimc&q=%1" + ).arg(currentSearchTerm); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void Technic::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList newList; + auto objs = doc["modpacks"].toArray(); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = technicPack.toObject(); + pack.name = technicPackObject["name"].toString(); + pack.slug = technicPackObject["slug"].toString(); + if (pack.slug == "vanilla") + continue; + if (technicPackObject["iconUrl"].isString()) + { + pack.logoUrl = technicPackObject["iconUrl"].toString(); + pack.logoName = pack.logoUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else + { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + pack.broken = false; + newList.append(pack); + } + searchState = Finished; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void Technic::ListModel::searchRequestFailed() +{ + jobPtr.reset(); + + if(searchState == ResetRequested) + { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + performSearch(); + } + else + { + searchState = Finished; + } +} + + +void Technic::ListModel::logoLoaded(QString logo, QString out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, QIcon(out)); + for(int i = 0; i < modpacks.size(); i++) + { + if(modpacks[i].logoName == logo) + { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void Technic::ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void Technic::ListModel::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") + { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); + NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + logoLoaded(logo, fullPath); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + job->start(); + + m_loadingLogos.append(logo); +} diff --git a/application/pages/modplatform/technic/TechnicModel.h b/application/pages/modplatform/technic/TechnicModel.h new file mode 100644 index 000000000..bd0aec69f --- /dev/null +++ b/application/pages/modplatform/technic/TechnicModel.h @@ -0,0 +1,70 @@ +/* Copyright 2020 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 + +#include "TechnicData.h" +#include "net/NetJob.h" + +namespace Technic { + +typedef std::function LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + virtual QVariant data(const QModelIndex& index, int role) const; + virtual int columnCount(const QModelIndex& parent) const; + virtual int rowCount(const QModelIndex& parent) const; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term); + +private slots: + void searchRequestFinished(); + void searchRequestFailed(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QString out); + +private: + void performSearch(); + void requestLogo(QString logo, QString url); + +private: + QList modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + QMap m_logoMap; + QMap waitingCallbacks; + + QString currentSearchTerm; + enum SearchState { + None, + ResetRequested, + Finished + } searchState = None; + NetJobPtr jobPtr; + QByteArray response; +}; + +} diff --git a/application/pages/modplatform/technic/TechnicPage.cpp b/application/pages/modplatform/technic/TechnicPage.cpp new file mode 100644 index 000000000..75efd3ed0 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.cpp @@ -0,0 +1,204 @@ +/* Copyright 2013-2020 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 "TechnicPage.h" +#include "ui_TechnicPage.h" + +#include "MultiMC.h" +#include "dialogs/NewInstanceDialog.h" +#include "TechnicModel.h" +#include +#include "modplatform/technic/SingleZipPackInstallTask.h" +#include "modplatform/technic/SolderPackInstallTask.h" +#include "Json.h" + +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + model = new Technic::ListModel(this); + ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); +} + +bool TechnicPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +TechnicPage::~TechnicPage() +{ + delete ui; +} + +bool TechnicPage::shouldDisplay() const +{ + return true; +} + +void TechnicPage::openedImpl() +{ + dialog->setSuggestedPack(); +} + +void TechnicPage::triggerSearch() { + model->searchWithTerm(ui->searchEdit->text()); +} + +void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + //ui->frame->clear(); + return; + } + + current = model->data(first, Qt::UserRole).value(); + suggestCurrent(); +} + +void TechnicPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + if (current.broken) + { + dialog->setSuggestedPack(); + return; + } + + QString editedLogoName; + editedLogoName = "technic_" + current.logoName.section(".", 0, 0); + model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + + if (current.metadataLoaded) + { + metadataLoaded(); + } + else + { + NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name)); + 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] + { + if (current.slug != slug) + { + return; + } + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonObject obj = doc.object(); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + if (!obj.contains("url")) + { + qWarning() << "Json doesn't contain an url key"; + return; + } + QJsonValueRef url = obj["url"]; + if (url.isString()) + { + current.url = url.toString(); + } + else + { + if (!obj.contains("solder")) + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + QJsonValueRef solderUrl = obj["solder"]; + if (solderUrl.isString()) + { + current.url = solderUrl.toString(); + current.isSolder = true; + } + else + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + } + + current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); + current.metadataLoaded = true; + metadataLoaded(); + }); + netJob->start(); + } +} + +// expects current.metadataLoaded to be true +void TechnicPage::metadataLoaded() +{ + /*QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + if (!current.authors.empty()) { + auto authorToStr = [](Technic::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += tr(" by ") + authorStrs.join(", "); + } + + ui->frame->setModText(text); + ui->frame->setModDescription(current.description);*/ + if (!current.isSolder) + { + dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + } + else + { + while (current.url.endsWith('/')) current.url.chop(1); + dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(current.url + "/modpack/" + current.slug, current.minecraftVersion)); + } +} diff --git a/application/pages/modplatform/technic/TechnicPage.h b/application/pages/modplatform/technic/TechnicPage.h new file mode 100644 index 000000000..1a10af716 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.h @@ -0,0 +1,78 @@ +/* Copyright 2013-2020 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 + +#include "pages/BasePage.h" +#include +#include "tasks/Task.h" +#include "TechnicData.h" + +namespace Ui +{ +class TechnicPage; +} + +class NewInstanceDialog; + +namespace Technic { + class ListModel; +} + +class TechnicPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~TechnicPage(); + virtual QString displayName() const override + { + return tr("Technic"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("technic"); + } + virtual QString id() const override + { + return "technic"; + } + virtual QString helpPage() const override + { + return "Technic-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + void suggestCurrent(); + void metadataLoaded(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + +private: + Ui::TechnicPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Technic::ListModel* model = nullptr; + Technic::Modpack current; +}; diff --git a/application/pages/modplatform/technic/TechnicPage.ui b/application/pages/modplatform/technic/TechnicPage.ui new file mode 100644 index 000000000..be56fa827 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.ui @@ -0,0 +1,62 @@ + + + TechnicPage + + + + 0 + 0 + 546 + 405 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Search + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + + diff --git a/application/pages/modplatform/twitch/TwitchModel.cpp b/application/pages/modplatform/twitch/TwitchModel.cpp index 9e3c3ad22..5c6c7858c 100644 --- a/application/pages/modplatform/twitch/TwitchModel.cpp +++ b/application/pages/modplatform/twitch/TwitchModel.cpp @@ -104,7 +104,7 @@ void ListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath] + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] { emit logoLoaded(logo, QIcon(fullPath)); if(waitingCallbacks.contains(logo)) diff --git a/application/resources/assets/underconstruction.png b/application/resources/assets/underconstruction.png new file mode 100644 index 0000000000000000000000000000000000000000..6ae06476eb256384ce693014bf2642365ba83220 GIT binary patch literal 14490 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 R?t``%db;|#taD0e0sswPW=sG8 literal 0 HcmV?d00001