Merge pull request #426 from flowln/mod_perma
Add on-disk mod metadata information
This commit is contained in:
		| @@ -643,6 +643,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", ""); | ||||
|  | ||||
|   | ||||
| @@ -324,19 +324,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 +502,9 @@ set(META_SOURCES | ||||
| ) | ||||
|  | ||||
| set(API_SOURCES | ||||
|     modplatform/ModIndex.h | ||||
|     modplatform/ModIndex.cpp | ||||
|  | ||||
|     modplatform/ModAPI.h | ||||
|  | ||||
|     modplatform/flame/FlameAPI.h | ||||
| @@ -545,6 +551,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 +615,7 @@ set(LOGIC_SOURCES | ||||
|     ${FLAME_SOURCES} | ||||
|     ${MODRINTH_SOURCES} | ||||
|     ${MODPACKSCH_SOURCES} | ||||
|     ${PACKWIZ_SOURCES} | ||||
|     ${TECHNIC_SOURCES} | ||||
|     ${ATLAUNCHER_SOURCES} | ||||
| ) | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,24 +1,47 @@ | ||||
| // 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 "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) | ||||
|     : m_mod(mod), m_mod_version(version), mods(mods) | ||||
| { | ||||
|     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 +55,3 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) | ||||
| { | ||||
|     emit progress(current, total); | ||||
| } | ||||
|  | ||||
| bool ModDownloadTask::abort() { | ||||
|     return m_filesNetJob->abort(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,28 +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 "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); | ||||
|     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); | ||||
|  | ||||
|   | ||||
| @@ -659,23 +659,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)"; | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|   | ||||
							
								
								
									
										59
									
								
								launcher/minecraft/mod/MetadataHandler.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								launcher/minecraft/mod/MetadataHandler.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }; | ||||
| @@ -1,24 +1,48 @@ | ||||
| /* 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. | ||||
| */ | ||||
|  | ||||
| #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 +50,69 @@ 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)) | ||||
|     // It is weird, but name is not reliable for comparing with the JAR files name | ||||
|     // FIXME: Maybe use hash when implemented? | ||||
|     , 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 +121,104 @@ 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) | ||||
| { | ||||
|     if(m_localDetails.get()) | ||||
|         m_localDetails->status = status; | ||||
| } | ||||
| void Mod::setMetadata(Metadata::ModStruct* metadata) | ||||
| { | ||||
|     if(status() == ModStatus::NoMetadata) | ||||
|         setStatus(ModStatus::Installed); | ||||
|  | ||||
|     if(m_localDetails.get()) | ||||
|         m_localDetails->metadata.reset(metadata); | ||||
| } | ||||
|  | ||||
| auto Mod::destroy(QDir& index_dir) -> bool | ||||
| { | ||||
|     auto n = name(); | ||||
|     // FIXME: This can fail to remove the metadata if the | ||||
|     // "DontUseModMetadata" 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()); | ||||
| } | ||||
|  | ||||
|  | ||||
| const ModDetails & Mod::details() const | ||||
| auto Mod::details() const -> const ModDetails& | ||||
| { | ||||
|     if(!m_localDetails) | ||||
|         return invalidDetails; | ||||
|     return *m_localDetails; | ||||
|     return m_localDetails ? *m_localDetails : invalidDetails; | ||||
| } | ||||
|  | ||||
|  | ||||
| QString Mod::version() const | ||||
| auto Mod::name() const -> QString | ||||
| { | ||||
|     return details().version; | ||||
| } | ||||
|  | ||||
| QString Mod::name() const | ||||
| { | ||||
|     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 | ||||
| { | ||||
|     return details().status; | ||||
| } | ||||
|  | ||||
| void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details) | ||||
| { | ||||
|     m_resolving = false; | ||||
|     m_resolved = true; | ||||
|     m_localDetails = details; | ||||
|  | ||||
|     if (status() != ModStatus::NoMetadata  | ||||
|             && m_temp_metadata.get() | ||||
|             && m_temp_metadata->isValid() &&  | ||||
|             m_localDetails.get()) { | ||||
|          | ||||
|         m_localDetails->metadata.swap(m_temp_metadata); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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,69 @@ 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() const -> const std::shared_ptr<Metadata::ModStruct> { return details().metadata; }; | ||||
|     auto metadata() -> std::shared_ptr<Metadata::ModStruct> { return m_localDetails->metadata; }; | ||||
|  | ||||
|     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; | ||||
|     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; | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
| }; | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -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; | ||||
| }; | ||||
| @@ -14,17 +14,19 @@ | ||||
|  */ | ||||
|  | ||||
| #include "ModFolderModel.h" | ||||
|  | ||||
| #include <FileSystem.h> | ||||
| #include <QDebug> | ||||
| #include <QFileSystemWatcher> | ||||
| #include <QMimeData> | ||||
| #include <QString> | ||||
| #include <QThreadPool> | ||||
| #include <QUrl> | ||||
| #include <QUuid> | ||||
| #include <QString> | ||||
| #include <QFileSystemWatcher> | ||||
| #include <QDebug> | ||||
| #include "ModFolderLoadTask.h" | ||||
| #include <QThreadPool> | ||||
| #include <algorithm> | ||||
| #include "LocalModParseTask.h" | ||||
|  | ||||
| #include "minecraft/mod/tasks/LocalModParseTask.h" | ||||
| #include "minecraft/mod/tasks/ModFolderLoadTask.h" | ||||
|  | ||||
| ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) | ||||
| { | ||||
| @@ -79,10 +81,14 @@ bool ModFolderModel::update() | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     auto task = new ModFolderLoadTask(m_dir); | ||||
|     auto index_dir = indexDir(); | ||||
|     auto task = new ModFolderLoadTask(dir(), index_dir); | ||||
|  | ||||
|     m_update = task->result(); | ||||
|  | ||||
|     QThreadPool *threadPool = QThreadPool::globalInstance(); | ||||
|     connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); | ||||
|      | ||||
|     threadPool->start(task); | ||||
|     return true; | ||||
| } | ||||
| @@ -153,7 +159,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 +180,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 +339,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 +391,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 +446,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? | ||||
|   | ||||
| @@ -24,8 +24,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; | ||||
| @@ -108,11 +108,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; | ||||
|   | ||||
| @@ -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) | ||||
| @@ -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 | ||||
| { | ||||
							
								
								
									
										53
									
								
								launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // 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 "LocalModUpdateTask.h" | ||||
|  | ||||
| #include "Application.h" | ||||
| #include "FileSystem.h" | ||||
| #include "minecraft/mod/MetadataHandler.h" | ||||
|  | ||||
| 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)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void LocalModUpdateTask::executeTask() | ||||
| { | ||||
|     setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); | ||||
|  | ||||
|     if(APPLICATION->settings()->get("DontUseModMetadata").toBool()){ | ||||
|         emitSucceeded(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
| } | ||||
							
								
								
									
										44
									
								
								launcher/minecraft/mod/tasks/LocalModUpdateTask.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								launcher/minecraft/mod/tasks/LocalModUpdateTask.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| }; | ||||
							
								
								
									
										82
									
								
								launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| // SPDX-License-Identifier: GPL-3.0-only | ||||
| /* | ||||
| *  PolyMC - Minecraft Launcher | ||||
| *  Copyright (c) 2022 flowln <flowlnlnln@gmail.com> | ||||
| * | ||||
| *  This program is free software: you can redistribute it and/or modify | ||||
| *  it under the terms of the GNU General Public License as published by | ||||
| *  the Free Software Foundation, version 3. | ||||
| * | ||||
| *  This program is distributed in the hope that it will be useful, | ||||
| *  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| *  GNU General Public License for more details. | ||||
| * | ||||
| *  You should have received a copy of the GNU General Public License | ||||
| *  along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| * | ||||
| * This file incorporates work covered by the following copyright and | ||||
| * permission notice: | ||||
| * | ||||
| *      Copyright 2013-2021 MultiMC Contributors | ||||
| * | ||||
| *      Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| *      you may not use this file except in compliance with the License. | ||||
| *      You may obtain a copy of the License at | ||||
| * | ||||
| *          http://www.apache.org/licenses/LICENSE-2.0 | ||||
| * | ||||
| *      Unless required by applicable law or agreed to in writing, software | ||||
| *      distributed under the License is distributed on an "AS IS" BASIS, | ||||
| *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| *      See the License for the specific language governing permissions and | ||||
| *      limitations under the License. | ||||
| */ | ||||
|  | ||||
| #include "ModFolderLoadTask.h" | ||||
|  | ||||
| #include "Application.h" | ||||
| #include "minecraft/mod/MetadataHandler.h" | ||||
|  | ||||
| ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir)  | ||||
|     : m_mods_dir(mods_dir), m_index_dir(index_dir), m_result(new Result()) | ||||
| {} | ||||
|  | ||||
| void ModFolderLoadTask::run() | ||||
| { | ||||
|     if (!APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { | ||||
|         // 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(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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										69
									
								
								launcher/minecraft/mod/tasks/ModFolderLoadTask.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								launcher/minecraft/mod/tasks/ModFolderLoadTask.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| // 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 <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); | ||||
|     void run(); | ||||
| signals: | ||||
|     void succeeded(); | ||||
|  | ||||
| private: | ||||
|     void getFromMetadata(); | ||||
|  | ||||
| private: | ||||
|     QDir& m_mods_dir, m_index_dir; | ||||
|     ResultPtr m_result; | ||||
| }; | ||||
							
								
								
									
										86
									
								
								launcher/modplatform/ModIndex.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								launcher/modplatform/ModIndex.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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,6 +26,19 @@ | ||||
|  | ||||
| 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; | ||||
| @@ -22,10 +53,13 @@ struct IndexedVersion { | ||||
|     QString date; | ||||
|     QString fileName; | ||||
|     QVector<QString> loaders = {}; | ||||
|     QString hash_type; | ||||
|     QString hash; | ||||
| }; | ||||
|  | ||||
| struct IndexedPack { | ||||
|     QVariant addonId; | ||||
|     Provider provider; | ||||
|     QString name; | ||||
|     QString description; | ||||
|     QList<ModpackAuthor> authors; | ||||
| @@ -40,3 +74,4 @@ struct IndexedPack { | ||||
| }  // namespace ModPlatform | ||||
|  | ||||
| Q_DECLARE_METATYPE(ModPlatform::IndexedPack) | ||||
| Q_DECLARE_METATYPE(ModPlatform::Provider) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "modplatform/ModIndex.h" | ||||
| #include "modplatform/helpers/NetworkModAPI.h" | ||||
|  | ||||
| class FlameAPI : public NetworkModAPI { | ||||
|   | ||||
| @@ -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", ""); | ||||
| @@ -27,6 +30,17 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) | ||||
|     } | ||||
| } | ||||
|  | ||||
| static QString enumToString(int hash_algorithm) | ||||
| { | ||||
|     switch(hash_algorithm){ | ||||
|     default: | ||||
|     case 1: | ||||
|         return "sha1"; | ||||
|     case 2: | ||||
|         return "md5"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, | ||||
|                                        QJsonArray& arr, | ||||
|                                        const shared_qobject_ptr<QNetworkAccessManager>& network, | ||||
| @@ -38,28 +52,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 +69,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; | ||||
| } | ||||
|   | ||||
| @@ -16,5 +16,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, | ||||
|                              QJsonArray& arr, | ||||
|                              const shared_qobject_ptr<QNetworkAccessManager>& network, | ||||
|                              BaseInstance* inst); | ||||
| auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; | ||||
|  | ||||
| }  // namespace FlameMod | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| #include "BuildConfig.h" | ||||
| #include "modplatform/ModAPI.h" | ||||
| #include "modplatform/ModIndex.h" | ||||
| #include "modplatform/helpers/NetworkModAPI.h" | ||||
|  | ||||
| #include <QDebug> | ||||
|   | ||||
| @@ -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", ""); | ||||
| @@ -57,46 +60,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 +73,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 {}; | ||||
| } | ||||
|   | ||||
| @@ -29,5 +29,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, | ||||
|                              QJsonArray& arr, | ||||
|                              const shared_qobject_ptr<QNetworkAccessManager>& network, | ||||
|                              BaseInstance* inst); | ||||
| auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; | ||||
|  | ||||
| }  // namespace Modrinth | ||||
|   | ||||
							
								
								
									
										289
									
								
								launcher/modplatform/packwiz/Packwiz.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								launcher/modplatform/packwiz/Packwiz.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										93
									
								
								launcher/modplatform/packwiz/Packwiz.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								launcher/modplatform/packwiz/Packwiz.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										86
									
								
								launcher/modplatform/packwiz/Packwiz_test.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								launcher/modplatform/packwiz/Packwiz_test.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <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(5); | ||||
|  | ||||
|         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" | ||||
							
								
								
									
										13
									
								
								launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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" | ||||
							
								
								
									
										13
									
								
								launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -64,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(status(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())); | ||||
|  | ||||
|     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))); | ||||
|  | ||||
|     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(); | ||||
| } | ||||
|  | ||||
| @@ -79,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) | ||||
| { | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -184,6 +184,11 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() | ||||
|     } | ||||
| } | ||||
|  | ||||
| void LauncherPage::on_metadataDisableBtn_clicked() | ||||
| { | ||||
|     ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); | ||||
| } | ||||
|  | ||||
| void LauncherPage::refreshUpdateChannelList() | ||||
| { | ||||
|     // Stop listening for selection changes. It's going to change a lot while we update it and | ||||
| @@ -338,6 +343,9 @@ void LauncherPage::applySettings() | ||||
|         s->set("InstSortMode", "Name"); | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     // Mods | ||||
|     s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); | ||||
| } | ||||
| void LauncherPage::loadSettings() | ||||
| { | ||||
| @@ -440,6 +448,10 @@ void LauncherPage::loadSettings() | ||||
|     { | ||||
|         ui->sortByNameBtn->setChecked(true); | ||||
|     } | ||||
|  | ||||
|     // Mods | ||||
|     ui->metadataDisableBtn->setChecked(s->get("DontUseModMetadata").toBool()); | ||||
|     ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); | ||||
| } | ||||
|  | ||||
| void LauncherPage::refreshFontPreview() | ||||
|   | ||||
| @@ -88,6 +88,7 @@ slots: | ||||
|     void on_instDirBrowseBtn_clicked(); | ||||
|     void on_modsDirBrowseBtn_clicked(); | ||||
|     void on_iconsDirBrowseBtn_clicked(); | ||||
|     void on_metadataDisableBtn_clicked(); | ||||
|  | ||||
|     /*! | ||||
|      * Updates the list of update channels in the combo box. | ||||
|   | ||||
| @@ -94,19 +94,13 @@ | ||||
|           <string>Folders</string> | ||||
|          </property> | ||||
|          <layout class="QGridLayout" name="foldersBoxLayout"> | ||||
|           <item row="0" column="0"> | ||||
|            <widget class="QLabel" name="labelInstDir"> | ||||
|           <item row="1" column="2"> | ||||
|            <widget class="QToolButton" name="modsDirBrowseBtn"> | ||||
|             <property name="text"> | ||||
|              <string>I&nstances:</string> | ||||
|             </property> | ||||
|             <property name="buddy"> | ||||
|              <cstring>instDirTextBox</cstring> | ||||
|              <string notr="true">...</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="1"> | ||||
|            <widget class="QLineEdit" name="instDirTextBox"/> | ||||
|           </item> | ||||
|           <item row="0" column="2"> | ||||
|            <widget class="QToolButton" name="instDirBrowseBtn"> | ||||
|             <property name="text"> | ||||
| @@ -114,28 +108,15 @@ | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="1" column="0"> | ||||
|            <widget class="QLabel" name="labelModsDir"> | ||||
|             <property name="text"> | ||||
|              <string>&Mods:</string> | ||||
|             </property> | ||||
|             <property name="buddy"> | ||||
|              <cstring>modsDirTextBox</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="2" column="2"> | ||||
|            <widget class="QToolButton" name="iconsDirBrowseBtn"> | ||||
|             <property name="text"> | ||||
|              <string notr="true">...</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="2" column="1"> | ||||
|            <widget class="QLineEdit" name="iconsDirTextBox"/> | ||||
|           <item row="0" column="1"> | ||||
|            <widget class="QLineEdit" name="instDirTextBox"/> | ||||
|           </item> | ||||
|           <item row="2" column="0"> | ||||
|            <widget class="QLabel" name="labelIconsDir"> | ||||
| @@ -147,10 +128,58 @@ | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="2" column="2"> | ||||
|            <widget class="QToolButton" name="iconsDirBrowseBtn"> | ||||
|           <item row="1" column="1"> | ||||
|            <widget class="QLineEdit" name="modsDirTextBox"/> | ||||
|           </item> | ||||
|           <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="1" column="0"> | ||||
|            <widget class="QLabel" name="labelModsDir"> | ||||
|             <property name="text"> | ||||
|              <string>&Mods:</string> | ||||
|             </property> | ||||
|             <property name="buddy"> | ||||
|              <cstring>modsDirTextBox</cstring> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|          </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>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> | ||||
|   | ||||
| @@ -150,7 +150,7 @@ 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)); | ||||
|         dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods)); | ||||
|     } | ||||
|  | ||||
|     updateSelectionButton(); | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user