NOISSUE asynchronous, parallel mod folder listing and mod resolving
This commit is contained in:
parent
7d13e31198
commit
a3ffa3d665
@ -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)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
298
api/logic/minecraft/mod/LocalModParseTask.cpp
Normal file
298
api/logic/minecraft/mod/LocalModParseTask.cpp
Normal file
@ -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);
|
||||
}
|
37
api/logic/minecraft/mod/LocalModParseTask.h
Normal file
37
api/logic/minecraft/mod/LocalModParseTask.h
Normal file
@ -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;
|
||||
};
|
169
api/logic/minecraft/mod/Mod.cpp
Normal file
169
api/logic/minecraft/mod/Mod.cpp
Normal file
@ -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;
|
||||
}
|
@ -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;
|
||||
};
|
17
api/logic/minecraft/mod/ModDetails.h
Normal file
17
api/logic/minecraft/mod/ModDetails.h
Normal file
@ -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;
|
||||
};
|
18
api/logic/minecraft/mod/ModFolderLoadTask.cpp
Normal file
18
api/logic/minecraft/mod/ModFolderLoadTask.cpp
Normal file
@ -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();
|
||||
}
|
29
api/logic/minecraft/mod/ModFolderLoadTask.h
Normal file
29
api/logic/minecraft/mod/ModFolderLoadTask.h
Normal file
@ -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;
|
||||
};
|
@ -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;
|
||||
|
||||
QList<Mod> newMods;
|
||||
m_dir.refresh();
|
||||
for (auto entry : m_dir.entryInfoList())
|
||||
{
|
||||
newMods.append(Mod(entry));
|
||||
}
|
||||
if(m_update) {
|
||||
scheduled_update = true;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
mods.swap(newMods);
|
||||
endResetModel();
|
||||
|
||||
emit changed();
|
||||
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 SimpleModList::disableInteraction(bool disabled)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// add new mods to the end
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
m_update.reset();
|
||||
|
||||
emit changed();
|
||||
|
||||
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 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)
|
||||
{
|
@ -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;
|
||||
};
|
@ -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"
|
@ -25,8 +25,8 @@
|
||||
#include "MultiMC.h"
|
||||
#include "dialogs/CustomMessageBox.h"
|
||||
#include <GuiUtil.h>
|
||||
#include "minecraft/SimpleModList.h"
|
||||
#include "minecraft/Mod.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "minecraft/VersionFilterData.h"
|
||||
#include "minecraft/ComponentList.h"
|
||||
#include <DesktopServices.h>
|
||||
@ -53,7 +53,7 @@ public:
|
||||
protected:
|
||||
bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override
|
||||
{
|
||||
SimpleModList *model = qobject_cast<SimpleModList *>(sourceModel());
|
||||
ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
|
||||
if(
|
||||
!model ||
|
||||
!source_left.isValid() ||
|
||||
@ -65,11 +65,11 @@ protected:
|
||||
|
||||
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed.
|
||||
|
||||
auto column = (SimpleModList::Columns) source_left.column();
|
||||
auto column = (ModFolderModel::Columns) source_left.column();
|
||||
bool invert = false;
|
||||
switch(column) {
|
||||
// GH-2550 - sort by enabled/disabled
|
||||
case SimpleModList::ActiveColumn: {
|
||||
case ModFolderModel::ActiveColumn: {
|
||||
auto dataL = source_left.data(Qt::CheckStateRole).toBool();
|
||||
auto dataR = source_right.data(Qt::CheckStateRole).toBool();
|
||||
if(dataL != dataR) {
|
||||
@ -79,10 +79,10 @@ protected:
|
||||
invert = sortOrder() == Qt::DescendingOrder;
|
||||
}
|
||||
// GH-2722 - sort mod names in a way that discards "The" prefixes
|
||||
case SimpleModList::NameColumn: {
|
||||
auto dataL = model->data(model->index(source_left.row(), SimpleModList::NameColumn)).toString();
|
||||
case ModFolderModel::NameColumn: {
|
||||
auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
|
||||
RemoveThePrefix(dataL);
|
||||
auto dataR = model->data(model->index(source_right.row(), SimpleModList::NameColumn)).toString();
|
||||
auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
|
||||
RemoveThePrefix(dataR);
|
||||
|
||||
auto less = dataL.compare(dataR, sortCaseSensitivity());
|
||||
@ -93,9 +93,9 @@ protected:
|
||||
invert = sortOrder() == Qt::DescendingOrder;
|
||||
}
|
||||
// GH-2762 - sort versions by parsing them as versions
|
||||
case SimpleModList::VersionColumn: {
|
||||
auto dataL = Version(model->data(model->index(source_left.row(), SimpleModList::VersionColumn)).toString());
|
||||
auto dataR = Version(model->data(model->index(source_right.row(), SimpleModList::VersionColumn)).toString());
|
||||
case ModFolderModel::VersionColumn: {
|
||||
auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
|
||||
auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
|
||||
return invert ? (dataL > dataR) : (dataL < dataR);
|
||||
}
|
||||
default: {
|
||||
@ -107,7 +107,7 @@ protected:
|
||||
|
||||
ModFolderPage::ModFolderPage(
|
||||
BaseInstance *inst,
|
||||
std::shared_ptr<SimpleModList> mods,
|
||||
std::shared_ptr<ModFolderModel> mods,
|
||||
QString id,
|
||||
QString iconName,
|
||||
QString displayName,
|
||||
@ -177,7 +177,7 @@ void ModFolderPage::on_filterTextChanged(const QString& newContents)
|
||||
}
|
||||
|
||||
|
||||
CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<SimpleModList> mods,
|
||||
CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods,
|
||||
QString id, QString iconName, QString displayName,
|
||||
QString helpPage, QWidget *parent)
|
||||
: ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent)
|
||||
|
@ -21,7 +21,7 @@
|
||||
#include "pages/BasePage.h"
|
||||
#include <MultiMC.h>
|
||||
|
||||
class SimpleModList;
|
||||
class ModFolderModel;
|
||||
namespace Ui
|
||||
{
|
||||
class ModFolderPage;
|
||||
@ -34,7 +34,7 @@ class ModFolderPage : public QMainWindow, public BasePage
|
||||
public:
|
||||
explicit ModFolderPage(
|
||||
BaseInstance *inst,
|
||||
std::shared_ptr<SimpleModList> mods,
|
||||
std::shared_ptr<ModFolderModel> mods,
|
||||
QString id,
|
||||
QString iconName,
|
||||
QString displayName,
|
||||
@ -78,7 +78,7 @@ protected:
|
||||
|
||||
protected:
|
||||
Ui::ModFolderPage *ui = nullptr;
|
||||
std::shared_ptr<SimpleModList> m_mods;
|
||||
std::shared_ptr<ModFolderModel> m_mods;
|
||||
QSortFilterProxyModel *m_filterModel = nullptr;
|
||||
QString m_iconName;
|
||||
QString m_id;
|
||||
@ -108,7 +108,7 @@ slots:
|
||||
class CoreModFolderPage : public ModFolderPage
|
||||
{
|
||||
public:
|
||||
explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<SimpleModList> mods, QString id,
|
||||
explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, QString id,
|
||||
QString iconName, QString displayName, QString helpPage = "",
|
||||
QWidget *parent = 0);
|
||||
virtual ~CoreModFolderPage()
|
||||
|
@ -39,7 +39,7 @@
|
||||
|
||||
#include "minecraft/ComponentList.h"
|
||||
#include "minecraft/auth/MojangAccountList.h"
|
||||
#include "minecraft/Mod.h"
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "icons/IconList.h"
|
||||
#include "Exception.h"
|
||||
#include "Version.h"
|
||||
|
@ -16,7 +16,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include "minecraft/Mod.h"
|
||||
#include "minecraft/mod/Mod.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user