diff options
Diffstat (limited to 'api/logic')
17 files changed, 802 insertions, 524 deletions
diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 1f795556..a762fb22 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -279,15 +279,21 @@ set(MINECRAFT_SOURCES minecraft/VersionFile.h minecraft/VersionFilterData.h minecraft/VersionFilterData.cpp - minecraft/Mod.h - minecraft/Mod.cpp - minecraft/SimpleModList.h - minecraft/SimpleModList.cpp minecraft/World.h minecraft/World.cpp minecraft/WorldList.h minecraft/WorldList.cpp + 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 + # Assets minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp @@ -318,8 +324,8 @@ add_unit_test(Library ) # FIXME: shares data with FileSystem test -add_unit_test(SimpleModList - SOURCES minecraft/SimpleModList_test.cpp +add_unit_test(ModFolderModel + SOURCES minecraft/mod/ModFolderModel_test.cpp DATA testdata LIBS MultiMC_logic ) @@ -479,8 +485,6 @@ set(LOGIC_SOURCES ${FLAME_SOURCES} ) -message(STATUS "FOO! ${LOGIC_SOURCES}") - add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) set_target_properties(MultiMC_logic PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1) diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index ee9c5cc1..85ac7802 100644 --- a/api/logic/MMCZip.h +++ b/api/logic/MMCZip.h @@ -18,7 +18,7 @@ #include <QString> #include <QFileInfo> #include <QSet> -#include "minecraft/Mod.h" +#include "minecraft/mod/Mod.h" #include <functional> #include "multimc_logic_export.h" diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 617d7431..9ca77798 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -26,7 +26,7 @@ #include "meta/Index.h" #include "meta/VersionList.h" -#include "SimpleModList.h" +#include "mod/ModFolderModel.h" #include "WorldList.h" #include "icons/IIconList.h" @@ -892,46 +892,46 @@ JavaVersion MinecraftInstance::getJavaVersion() const return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr<SimpleModList> MinecraftInstance::loaderModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new SimpleModList(loaderModsDir())); + m_loader_mod_list.reset(new ModFolderModel(loaderModsDir())); m_loader_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &SimpleModList::disableInteraction); + connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } return m_loader_mod_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::coreModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new SimpleModList(coreModsDir())); + m_core_mod_list.reset(new ModFolderModel(coreModsDir())); m_core_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &SimpleModList::disableInteraction); + connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } return m_core_mod_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::resourcePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new SimpleModList(resourcePacksDir())); + m_resource_pack_list.reset(new ModFolderModel(resourcePacksDir())); m_resource_pack_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &SimpleModList::disableInteraction); + connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction); } return m_resource_pack_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::texturePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new SimpleModList(texturePacksDir())); + m_texture_pack_list.reset(new ModFolderModel(texturePacksDir())); m_texture_pack_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &SimpleModList::disableInteraction); + connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction); } return m_texture_pack_list; } diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index 501697f7..dd14664f 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -1,13 +1,13 @@ #pragma once #include "BaseInstance.h" #include <java/JavaVersion.h> -#include "minecraft/Mod.h" +#include "minecraft/mod/Mod.h" #include <QProcess> #include <QDir> #include "multimc_logic_export.h" class ModsModel; -class SimpleModList; +class ModFolderModel; class WorldList; class GameOptions; class LaunchStep; @@ -69,10 +69,10 @@ public: ////// Mod Lists ////// std::shared_ptr<ModsModel> modsModel() const; - std::shared_ptr<SimpleModList> loaderModList() const; - std::shared_ptr<SimpleModList> coreModList() const; - std::shared_ptr<SimpleModList> resourcePackList() const; - std::shared_ptr<SimpleModList> texturePackList() const; + std::shared_ptr<ModFolderModel> loaderModList() const; + std::shared_ptr<ModFolderModel> coreModList() const; + std::shared_ptr<ModFolderModel> resourcePackList() const; + std::shared_ptr<ModFolderModel> texturePackList() const; std::shared_ptr<WorldList> worldList() const; std::shared_ptr<GameOptions> gameOptionsModel() const; @@ -124,10 +124,10 @@ private: protected: // data std::shared_ptr<ComponentList> m_components; mutable std::shared_ptr<ModsModel> m_mods_model; - mutable std::shared_ptr<SimpleModList> m_loader_mod_list; - mutable std::shared_ptr<SimpleModList> m_core_mod_list; - mutable std::shared_ptr<SimpleModList> m_resource_pack_list; - mutable std::shared_ptr<SimpleModList> m_texture_pack_list; + mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; + mutable std::shared_ptr<ModFolderModel> m_core_mod_list; + mutable std::shared_ptr<ModFolderModel> m_resource_pack_list; + mutable std::shared_ptr<ModFolderModel> m_texture_pack_list; mutable std::shared_ptr<WorldList> m_world_list; mutable std::shared_ptr<GameOptions> m_game_options; }; diff --git a/api/logic/minecraft/Mod.cpp b/api/logic/minecraft/Mod.cpp deleted file mode 100644 index 936ca00a..00000000 --- a/api/logic/minecraft/Mod.cpp +++ /dev/null @@ -1,433 +0,0 @@ -/* Copyright 2013-2019 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <QDir> -#include <QString> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> -#include <QJsonValue> -#include <quazip.h> -#include <quazipfile.h> - -#include "Mod.h" -#include "settings/INIFile.h" -#include <FileSystem.h> -#include <QDebug> - -namespace { -// NEW format -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 - -// OLD format: -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc -ModDetails ReadMCModInfo(QByteArray contents) -{ - auto getInfoFromArray = [&](QJsonArray arr)->ModDetails - { - ModDetails details; - if (!arr.at(0).isObject()) { - return details; - } - auto firstObj = arr.at(0).toObject(); - details.mod_id = firstObj.value("modid").toString(); - auto name = firstObj.value("name").toString(); - // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name - if(name != "Example Mod") { - 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()) - { - // fix up url. - if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) - { - homeurl.prepend("http://"); - } - } - details.homeurl = homeurl; - details.description = firstObj.value("description").toString(); - QJsonArray authors = firstObj.value("authorList").toArray(); - if (authors.size() == 0) { - // FIXME: what is the format of this? is there any? - authors = firstObj.value("authors").toArray(); - } - - for (auto author: authors) - { - details.authors.append(author.toString()); - } - details.credits = firstObj.value("credits").toString(); - details.valid = true; - return details; - }; - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - // this is the very old format that had just the array - if (jsonDoc.isArray()) - { - return getInfoFromArray(jsonDoc.array()); - } - else if (jsonDoc.isObject()) - { - auto val = jsonDoc.object().value("modinfoversion"); - if(val.isUndefined()) { - val = jsonDoc.object().value("modListVersion"); - } - int version = val.toDouble(); - if (version != 2) - { - qCritical() << "BAD stuff happened to mod json:"; - qCritical() << contents; - return ModDetails(); - } - auto arrVal = jsonDoc.object().value("modlist"); - if(arrVal.isUndefined()) { - arrVal = jsonDoc.object().value("modList"); - } - if (arrVal.isArray()) - { - return getInfoFromArray(arrVal.toArray()); - } - } - return ModDetails(); -} - -// https://fabricmc.net/wiki/documentation:fabric_mod_json -ModDetails ReadFabricModInfo(QByteArray contents) -{ - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - auto object = jsonDoc.object(); - auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; - - ModDetails details; - - details.mod_id = object.value("id").toString(); - details.version = object.value("version").toString(); - - details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; - details.description = object.value("description").toString(); - - if (schemaVersion >= 1) - { - QJsonArray authors = object.value("authors").toArray(); - for (auto author: authors) - { - if(author.isObject()) { - details.authors.append(author.toObject().value("name").toString()); - } - else { - details.authors.append(author.toString()); - } - } - - if (object.contains("contact")) - { - QJsonObject contact = object.value("contact").toObject(); - - if (contact.contains("homepage")) - { - details.homeurl = contact.value("homepage").toString(); - } - } - } - details.valid = !details.name.isEmpty(); - return details; -} - -ModDetails ReadForgeInfo(QByteArray contents) -{ - ModDetails details; - // Read the data - details.name = "Minecraft Forge"; - details.mod_id = "Forge"; - details.homeurl = "http://www.minecraftforge.net/forum/"; - details.valid = true; - INIFile ini; - if (!ini.loadFile(contents)) - return details; - - QString major = ini.get("forge.major.number", "0").toString(); - QString minor = ini.get("forge.minor.number", "0").toString(); - QString revision = ini.get("forge.revision.number", "0").toString(); - QString build = ini.get("forge.build.number", "0").toString(); - - details.version = major + "." + minor + "." + revision + "." + build; - return details; -} - -ModDetails ReadLiteModInfo(QByteArray contents) -{ - ModDetails details; - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - auto object = jsonDoc.object(); - if (object.contains("name")) - { - details.mod_id = details.name = object.value("name").toString(); - } - if (object.contains("version")) - { - details.version = object.value("version").toString(""); - } - else - { - details.version = object.value("revision").toString(""); - } - details.mcversion = object.value("mcversion").toString(); - auto author = object.value("author").toString(); - if(!author.isEmpty()) { - details.authors.append(author); - } - details.description = object.value("description").toString(); - details.homeurl = object.value("url").toString(); - return details; -} - -ModDetails invalidDetails; - -} - - -Mod::Mod(const QFileInfo &file) -{ - repath(file); - m_changedDateTime = file.lastModified(); -} - -void Mod::repath(const QFileInfo &file) -{ - m_file = file; - QString name_base = file.fileName(); - - m_type = Mod::MOD_UNKNOWN; - - if (m_file.isDir()) - { - m_type = MOD_FOLDER; - m_name = name_base; - m_mmc_id = name_base; - } - else if (m_file.isFile()) - { - if (name_base.endsWith(".disabled")) - { - m_enabled = false; - name_base.chop(9); - } - else - { - m_enabled = true; - } - m_mmc_id = name_base; - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) - { - m_type = MOD_ZIPFILE; - name_base.chop(4); - } - else if (name_base.endsWith(".litemod")) - { - m_type = MOD_LITEMOD; - name_base.chop(8); - } - else - { - m_type = MOD_SINGLEFILE; - } - m_name = name_base; - } - - if (m_type == MOD_ZIPFILE) - { - QuaZip zip(m_file.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("mcmod.info")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_localDetails = ReadMCModInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - else if (zip.setCurrentFile("fabric.mod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_localDetails = ReadFabricModInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - else if (zip.setCurrentFile("forgeversion.properties")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_localDetails = ReadForgeInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - - zip.close(); - } - else if (m_type == MOD_FOLDER) - { - QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info")); - if (mcmod_info.isFile()) - { - QFile mcmod(mcmod_info.filePath()); - if (!mcmod.open(QIODevice::ReadOnly)) - return; - auto data = mcmod.readAll(); - if (data.isEmpty() || data.isNull()) - return; - m_localDetails = ReadMCModInfo(data); - } - } - else if (m_type == MOD_LITEMOD) - { - QuaZip zip(m_file.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("litemod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_localDetails = ReadLiteModInfo(file.readAll()); - file.close(); - } - zip.close(); - } -} - -bool Mod::enable(bool value) -{ - if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) - return false; - - if (m_enabled == value) - return false; - - QString path = m_file.absoluteFilePath(); - if (value) - { - QFile foo(path); - if (!path.endsWith(".disabled")) - return false; - path.chop(9); - if (!foo.rename(path)) - return false; - } - else - { - QFile foo(path); - path += ".disabled"; - if (!foo.rename(path)) - return false; - } - m_file = QFileInfo(path); - m_enabled = value; - return true; -} - -bool Mod::destroy() -{ - if (m_type == MOD_FOLDER) - { - QDir d(m_file.filePath()); - if (d.removeRecursively()) - { - m_type = MOD_UNKNOWN; - return true; - } - return false; - } - else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) - { - QFile f(m_file.filePath()); - if (f.remove()) - { - m_type = MOD_UNKNOWN; - return true; - } - return false; - } - return true; -} - - -const ModDetails & Mod::details() const -{ - if(!m_localDetails) - return invalidDetails; - return m_localDetails; -} - - -QString Mod::version() const -{ - return details().version; -} - -QString Mod::name() const -{ - auto & d = details(); - if(d && !d.name.isEmpty()) { - return d.name; - } - return m_name; -} - -QString Mod::homeurl() const -{ - return details().homeurl; -} - -QString Mod::description() const -{ - return details().description; -} - -QStringList Mod::authors() const -{ - return details().authors; -} diff --git a/api/logic/minecraft/ParseUtils_test.cpp b/api/logic/minecraft/ParseUtils_test.cpp index fde9cdbf..fcc137e5 100644 --- a/api/logic/minecraft/ParseUtils_test.cpp +++ b/api/logic/minecraft/ParseUtils_test.cpp @@ -33,7 +33,7 @@ slots: auto time_parsed = timeFromS3Time(timestamp); auto time_serialized = timeToS3Time(time_parsed); - + QCOMPARE(time_serialized, timestamp); } diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h index 46fca3e4..7c0b94e8 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -16,12 +16,11 @@ #pragma once #include "BaseInstance.h" -#include "minecraft/Mod.h" #include "launch/LaunchTask.h" #include "multimc_logic_export.h" -class SimpleModList; +class ModFolderModel; class LegacyModList; class WorldList; class Task; diff --git a/api/logic/minecraft/mod/LocalModParseTask.cpp b/api/logic/minecraft/mod/LocalModParseTask.cpp new file mode 100644 index 00000000..22ebd7d4 --- /dev/null +++ b/api/logic/minecraft/mod/LocalModParseTask.cpp @@ -0,0 +1,298 @@ +#include "LocalModParseTask.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <quazip.h> +#include <quazipfile.h> + +#include "settings/INIFile.h" +#include "FileSystem.h" + +namespace { + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails> + { + if (!arr.at(0).isObject()) { + return nullptr; + } + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + auto firstObj = arr.at(0).toObject(); + details->mod_id = firstObj.value("modid").toString(); + auto name = firstObj.value("name").toString(); + // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name + if(name != "Example Mod") { + 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()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + details->description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) { + // FIXME: what is the format of this? is there any? + authors = firstObj.value("authors").toArray(); + } + + for (auto author: authors) + { + details->authors.append(author.toString()); + } + details->credits = firstObj.value("credits").toString(); + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + return getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) { + val = jsonDoc.object().value("modListVersion"); + } + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return nullptr; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) + { + return getInfoFromArray(arrVal.toArray()); + } + } + return nullptr; +} + +// https://fabricmc.net/wiki/documentation:fabric_mod_json +std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; + + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + + details->mod_id = object.value("id").toString(); + details->version = object.value("version").toString(); + + details->name = object.contains("name") ? object.value("name").toString() : details->mod_id; + details->description = object.value("description").toString(); + + if (schemaVersion >= 1) + { + QJsonArray authors = object.value("authors").toArray(); + for (auto author: authors) + { + if(author.isObject()) { + details->authors.append(author.toObject().value("name").toString()); + } + else { + details->authors.append(author.toString()); + } + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details->homeurl = contact.value("homepage").toString(); + } + } + } + return details; +} + +std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + // Read the data + details->name = "Minecraft Forge"; + details->mod_id = "Forge"; + details->homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details->version = major + "." + minor + "." + revision + "." + build; + return details; +} + +std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + details->mod_id = details->name = object.value("name").toString(); + } + if (object.contains("version")) + { + details->version = object.value("version").toString(""); + } + else + { + details->version = object.value("revision").toString(""); + } + details->mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if(!author.isEmpty()) { + details->authors.append(author); + } + details->description = object.value("description").toString(); + details->homeurl = object.value("url").toString(); + return details; +} + +} + +LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile): + m_token(token), + m_type(type), + m_modFile(modFile), + m_result(new Result()) +{ +} + +void LocalModParseTask::processAsZip() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); +} + +void LocalModParseTask::processAsFolder() +{ + QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + m_result->details = ReadMCModInfo(data); + } +} + +void LocalModParseTask::processAsLitemod() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); +} + +void LocalModParseTask::run() +{ + switch(m_type) + { + case Mod::MOD_ZIPFILE: + processAsZip(); + break; + case Mod::MOD_FOLDER: + processAsFolder(); + break; + case Mod::MOD_LITEMOD: + processAsLitemod(); + break; + default: + break; + } + emit finished(m_token); +} diff --git a/api/logic/minecraft/mod/LocalModParseTask.h b/api/logic/minecraft/mod/LocalModParseTask.h new file mode 100644 index 00000000..0f119ba6 --- /dev/null +++ b/api/logic/minecraft/mod/LocalModParseTask.h @@ -0,0 +1,37 @@ +#pragma once +#include <QRunnable> +#include <QDebug> +#include <QObject> +#include "Mod.h" +#include "ModDetails.h" + +class LocalModParseTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QString id; + std::shared_ptr<ModDetails> details; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + + LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile); + void run(); + +signals: + void finished(int token); + +private: + void processAsZip(); + void processAsFolder(); + void processAsLitemod(); + +private: + int m_token; + Mod::ModType m_type; + QFileInfo m_modFile; + ResultPtr m_result; +}; diff --git a/api/logic/minecraft/mod/Mod.cpp b/api/logic/minecraft/mod/Mod.cpp new file mode 100644 index 00000000..aa2496c2 --- /dev/null +++ b/api/logic/minecraft/mod/Mod.cpp @@ -0,0 +1,169 @@ +/* Copyright 2013-2019 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QDir> +#include <QString> + +#include "Mod.h" +#include <QDebug> + +namespace { + +ModDetails invalidDetails; + +} + + +Mod::Mod(const QFileInfo &file) +{ + repath(file); + m_changedDateTime = file.lastModified(); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + m_mmc_id = name_base; + + if (m_file.isDir()) + { + m_type = MOD_FOLDER; + m_name = name_base; + } + else if (m_file.isFile()) + { + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } + else + { + m_enabled = true; + } + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + m_file = QFileInfo(path); + m_enabled = value; + return true; +} + +bool Mod::destroy() +{ + if (m_type == MOD_FOLDER) + { + QDir d(m_file.filePath()); + if (d.removeRecursively()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) + { + QFile f(m_file.filePath()); + if (f.remove()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + return true; +} + + +const ModDetails & Mod::details() const +{ + if(!m_localDetails) + return invalidDetails; + return *m_localDetails; +} + + +QString Mod::version() const +{ + return details().version; +} + +QString Mod::name() const +{ + auto & d = details(); + if(!d.name.isEmpty()) { + return d.name; + } + return m_name; +} + +QString Mod::homeurl() const +{ + return details().homeurl; +} + +QString Mod::description() const +{ + return details().description; +} + +QStringList Mod::authors() const +{ + return details().authors; +} diff --git a/api/logic/minecraft/Mod.h b/api/logic/minecraft/mod/Mod.h index 890669ce..d787fb48 100644 --- a/api/logic/minecraft/Mod.h +++ b/api/logic/minecraft/mod/Mod.h @@ -16,24 +16,14 @@ #pragma once #include <QFileInfo> #include <QDateTime> +#include <QList> +#include <memory> + #include "multimc_logic_export.h" -struct ModDetails -{ - operator bool() const { - return valid; - } - bool valid = false; - QString mod_id; - QString name; - QString version; - QString mcversion; - QString homeurl; - QString updateurl; - QString description; - QStringList authors; - QString credits; -}; +#include "ModDetails.h" + + class MULTIMC_LOGIC_EXPORT Mod { @@ -47,6 +37,7 @@ public: MOD_LITEMOD, //!< The mod is a litemod }; + Mod() = default; Mod(const QFileInfo &file); QFileInfo filename() const @@ -92,13 +83,35 @@ public: // 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; + } + 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; + } + protected: QFileInfo m_file; QDateTime m_changedDateTime; QString m_mmc_id; QString m_name; bool m_enabled = true; + bool m_resolving = false; + bool m_resolved = false; + int m_resolutionTicket = 0; ModType m_type = MOD_UNKNOWN; - bool m_bare = true; - ModDetails m_localDetails; + std::shared_ptr<ModDetails> m_localDetails; }; diff --git a/api/logic/minecraft/mod/ModDetails.h b/api/logic/minecraft/mod/ModDetails.h new file mode 100644 index 00000000..6ab4aee7 --- /dev/null +++ b/api/logic/minecraft/mod/ModDetails.h @@ -0,0 +1,17 @@ +#pragma once + +#include <QString> +#include <QStringList> + +struct ModDetails +{ + QString mod_id; + QString name; + QString version; + QString mcversion; + QString homeurl; + QString updateurl; + QString description; + QStringList authors; + QString credits; +}; diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.cpp b/api/logic/minecraft/mod/ModFolderLoadTask.cpp new file mode 100644 index 00000000..88349877 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderLoadTask.cpp @@ -0,0 +1,18 @@ +#include "ModFolderLoadTask.h" +#include <QDebug> + +ModFolderLoadTask::ModFolderLoadTask(QDir dir) : + m_dir(dir), m_result(new Result()) +{ +} + +void ModFolderLoadTask::run() +{ + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) + { + Mod m(entry); + m_result->mods[m.mmc_id()] = m; + } + emit succeeded(); +} diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.h b/api/logic/minecraft/mod/ModFolderLoadTask.h new file mode 100644 index 00000000..8d720e65 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderLoadTask.h @@ -0,0 +1,29 @@ +#pragma once +#include <QRunnable> +#include <QObject> +#include <QDir> +#include <QMap> +#include "Mod.h" +#include <memory> + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QMap<QString, Mod> mods; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + +public: + ModFolderLoadTask(QDir dir); + void run(); +signals: + void succeeded(); +private: + QDir m_dir; + ResultPtr m_result; +}; diff --git a/api/logic/minecraft/SimpleModList.cpp b/api/logic/minecraft/mod/ModFolderModel.cpp index b90b55c2..7293f837 100644 --- a/api/logic/minecraft/SimpleModList.cpp +++ b/api/logic/minecraft/mod/ModFolderModel.cpp @@ -13,7 +13,7 @@ * limitations under the License. */ -#include "SimpleModList.h" +#include "ModFolderModel.h" #include <FileSystem.h> #include <QMimeData> #include <QUrl> @@ -21,8 +21,12 @@ #include <QString> #include <QFileSystemWatcher> #include <QDebug> +#include "ModFolderLoadTask.h" +#include <QThreadPool> +#include <algorithm> +#include "LocalModParseTask.h" -SimpleModList::SimpleModList(const QString &dir) : QAbstractListModel(), m_dir(dir) +ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); @@ -31,7 +35,7 @@ SimpleModList::SimpleModList(const QString &dir) : QAbstractListModel(), m_dir(d connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); } -void SimpleModList::startWatching() +void ModFolderModel::startWatching() { if(is_watching) return; @@ -49,7 +53,7 @@ void SimpleModList::startWatching() } } -void SimpleModList::stopWatching() +void ModFolderModel::stopWatching() { if(!is_watching) return; @@ -65,27 +69,136 @@ void SimpleModList::stopWatching() } } -bool SimpleModList::update() +bool ModFolderModel::update() { - if (!isValid()) + if (!isValid()) { return false; + } + if(m_update) { + scheduled_update = true; + } + + auto task = new ModFolderLoadTask(m_dir); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::updateFinished); + threadPool->start(task); + return true; +} + +void ModFolderModel::updateFinished() +{ + QSet<QString> currentSet = modsIndex.keys().toSet(); + auto & newMods = m_update->mods; + QSet<QString> newSet = newMods.keys().toSet(); + + // see if the kept mods changed in some way + { + QSet<QString> kept = currentSet; + kept.intersect(newSet); + for(auto & keptMod: kept) { + auto & newMod = newMods[keptMod]; + auto row = modsIndex[keptMod]; + auto & currentMod = mods[row]; + if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) { + // no significant change, ignore... + continue; + } + auto & oldMod = mods[row]; + if(oldMod.isResolving()) { + activeTickets.remove(oldMod.resolutionTicket()); + } + oldMod = newMod; + resolveMod(mods[row]); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove mods no longer present + { + QSet<QString> removed = currentSet; + QList<int> removedRows; + removed.subtract(newSet); + for(auto & removedMod: removed) { + removedRows.append(modsIndex[removedMod]); + } + std::sort(removedRows.begin(), removedRows.end()); + for(auto iter = removedRows.rbegin(); iter != removedRows.rend(); iter++) { + int removedIndex = *iter; + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + auto removedIter = mods.begin() + removedIndex; + if(removedIter->isResolving()) { + activeTickets.remove(removedIter->resolutionTicket()); + } + mods.erase(removedIter); + endRemoveRows(); + } + } - QList<Mod> newMods; - m_dir.refresh(); - for (auto entry : m_dir.entryInfoList()) + // add new mods to the end { - newMods.append(Mod(entry)); + QSet<QString> added = newSet; + added.subtract(currentSet); + beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); + for(auto & addedMod: added) { + mods.append(newMods[addedMod]); + resolveMod(mods.last()); + } + endInsertRows(); + } + + // update index + { + modsIndex.clear(); + int idx = 0; + for(auto & mod: mods) { + modsIndex[mod.mmc_id()] = idx; + idx++; + } } - beginResetModel(); - mods.swap(newMods); - endResetModel(); + m_update.reset(); emit changed(); - return true; + + if(scheduled_update) { + scheduled_update = false; + update(); + } +} + +void ModFolderModel::resolveMod(Mod& m) +{ + if(!m.shouldResolve()) { + return; + } + + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto result = task->result(); + result->id = m.mmc_id(); + activeTickets.insert(nextResolutionTicket, result); + m.setResolving(true, nextResolutionTicket); + nextResolutionTicket++; + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &LocalModParseTask::finished, this, &ModFolderModel::modParseFinished); + threadPool->start(task); +} + +void ModFolderModel::modParseFinished(int token) +{ + auto iter = activeTickets.find(token); + if(iter == activeTickets.end()) { + return; + } + auto result = *iter; + activeTickets.remove(token); + int row = modsIndex[result->id]; + auto & mod = mods[row]; + mod.finishResolvingWithDetails(result->details); + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } -void SimpleModList::disableInteraction(bool disabled) +void ModFolderModel::disableInteraction(bool disabled) { if (interaction_disabled == disabled) { return; @@ -96,18 +209,18 @@ void SimpleModList::disableInteraction(bool disabled) } } -void SimpleModList::directoryChanged(QString path) +void ModFolderModel::directoryChanged(QString path) { update(); } -bool SimpleModList::isValid() +bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); } // FIXME: this does not take disabled mod (with extra .disable extension) into account... -bool SimpleModList::installMod(const QString &filename) +bool ModFolderModel::installMod(const QString &filename) { if(interaction_disabled) { return false; @@ -189,7 +302,7 @@ bool SimpleModList::installMod(const QString &filename) return false; } -bool SimpleModList::enableMods(const QModelIndexList& indexes, bool enable) +bool ModFolderModel::enableMods(const QModelIndexList& indexes, bool enable) { if(interaction_disabled) { return false; @@ -208,7 +321,7 @@ bool SimpleModList::enableMods(const QModelIndexList& indexes, bool enable) return true; } -bool SimpleModList::deleteMods(const QModelIndexList& indexes) +bool ModFolderModel::deleteMods(const QModelIndexList& indexes) { if(interaction_disabled) { return false; @@ -226,12 +339,12 @@ bool SimpleModList::deleteMods(const QModelIndexList& indexes) return true; } -int SimpleModList::columnCount(const QModelIndex &parent) const +int ModFolderModel::columnCount(const QModelIndex &parent) const { return NUM_COLUMNS; } -QVariant SimpleModList::data(const QModelIndex &index, int role) const +QVariant ModFolderModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); @@ -283,7 +396,7 @@ QVariant SimpleModList::data(const QModelIndex &index, int role) const } } -bool SimpleModList::setData(const QModelIndex &index, const QVariant &value, int role) +bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) { @@ -302,7 +415,7 @@ bool SimpleModList::setData(const QModelIndex &index, const QVariant &value, int return false; } -QVariant SimpleModList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { switch (role) { @@ -341,7 +454,7 @@ QVariant SimpleModList::headerData(int section, Qt::Orientation orientation, int return QVariant(); } -Qt::ItemFlags SimpleModList::flags(const QModelIndex &index) const +Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); auto flags = defaultFlags; @@ -358,20 +471,20 @@ Qt::ItemFlags SimpleModList::flags(const QModelIndex &index) const return flags; } -Qt::DropActions SimpleModList::supportedDropActions() const +Qt::DropActions ModFolderModel::supportedDropActions() const { // copy from outside, move from within and other mod lists return Qt::CopyAction | Qt::MoveAction; } -QStringList SimpleModList::mimeTypes() const +QStringList ModFolderModel::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } -bool SimpleModList::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) { if (action == Qt::IgnoreAction) { diff --git a/api/logic/minecraft/SimpleModList.h b/api/logic/minecraft/mod/ModFolderModel.h index 8cb57727..776c0c87 100644 --- a/api/logic/minecraft/SimpleModList.h +++ b/api/logic/minecraft/mod/ModFolderModel.h @@ -16,13 +16,17 @@ #pragma once #include <QList> +#include <QMap> +#include <QSet> #include <QString> #include <QDir> #include <QAbstractListModel> -#include "minecraft/Mod.h" +#include "Mod.h" #include "multimc_logic_export.h" +#include "ModFolderLoadTask.h" +#include "LocalModParseTask.h" class LegacyInstance; class BaseInstance; @@ -32,7 +36,7 @@ class QFileSystemWatcher; * A legacy mod list. * Backed by a folder. */ -class MULTIMC_LOGIC_EXPORT SimpleModList : public QAbstractListModel +class MULTIMC_LOGIC_EXPORT ModFolderModel : public QAbstractListModel { Q_OBJECT public: @@ -40,11 +44,11 @@ public: { ActiveColumn = 0, NameColumn, - DateColumn, VersionColumn, + DateColumn, NUM_COLUMNS }; - SimpleModList(const QString &dir); + ModFolderModel(const QString &dir); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; @@ -112,14 +116,24 @@ public slots: private slots: void directoryChanged(QString path); + void updateFinished(); + void modParseFinished(int token); signals: void changed(); +private: + void resolveMod(Mod& m); + protected: QFileSystemWatcher *m_watcher; bool is_watching = false; + ModFolderLoadTask::ResultPtr m_update; + bool scheduled_update = false; bool interaction_disabled = false; QDir m_dir; + QMap<QString, int> modsIndex; + QMap<int, LocalModParseTask::ResultPtr> activeTickets; + int nextResolutionTicket = 0; QList<Mod> mods; }; diff --git a/api/logic/minecraft/SimpleModList_test.cpp b/api/logic/minecraft/mod/ModFolderModel_test.cpp index a100b539..76f16ed5 100644 --- a/api/logic/minecraft/SimpleModList_test.cpp +++ b/api/logic/minecraft/mod/ModFolderModel_test.cpp @@ -4,9 +4,9 @@ #include "TestUtil.h" #include "FileSystem.h" -#include "minecraft/SimpleModList.h" +#include "minecraft/mod/ModFolderModel.h" -class SimpleModListTest : public QObject +class ModFolderModelTest : public QObject { Q_OBJECT @@ -32,7 +32,7 @@ slots: { QString folder = source; QTemporaryDir tempDir; - SimpleModList m(tempDir.path()); + ModFolderModel m(tempDir.path()); m.installMod(folder); verify(tempDir.path()); } @@ -41,13 +41,13 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; - SimpleModList m(tempDir.path()); + ModFolderModel m(tempDir.path()); m.installMod(folder); verify(tempDir.path()); } } }; -QTEST_GUILESS_MAIN(SimpleModListTest) +QTEST_GUILESS_MAIN(ModFolderModelTest) -#include "SimpleModList_test.moc" +#include "ModFolderModel_test.moc" |