NOISSUE Flatten gui and logic libraries into MultiMC
This commit is contained in:
467
launcher/minecraft/mod/LocalModParseTask.cpp
Normal file
467
launcher/minecraft/mod/LocalModParseTask.cpp
Normal file
@ -0,0 +1,467 @@
|
||||
#include "LocalModParseTask.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <quazip.h>
|
||||
#include <quazipfile.h>
|
||||
#include <toml.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://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
|
||||
std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
{
|
||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
||||
|
||||
char errbuf[200];
|
||||
// top-level table
|
||||
toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf));
|
||||
|
||||
if(!tomlData)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// array defined by [[mods]]
|
||||
toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods");
|
||||
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
|
||||
toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0);
|
||||
|
||||
// mandatory properties - always in [[mods]]
|
||||
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
|
||||
if(modIdDatum.ok)
|
||||
{
|
||||
details->mod_id = modIdDatum.u.s;
|
||||
// library says this is required for strings
|
||||
free(modIdDatum.u.s);
|
||||
}
|
||||
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
|
||||
if(versionDatum.ok)
|
||||
{
|
||||
details->version = versionDatum.u.s;
|
||||
free(versionDatum.u.s);
|
||||
}
|
||||
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
|
||||
if(displayNameDatum.ok)
|
||||
{
|
||||
details->name = displayNameDatum.u.s;
|
||||
free(displayNameDatum.u.s);
|
||||
}
|
||||
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
|
||||
if(descriptionDatum.ok)
|
||||
{
|
||||
details->description = descriptionDatum.u.s;
|
||||
free(descriptionDatum.u.s);
|
||||
}
|
||||
|
||||
// optional properties - can be in the root table or [[mods]]
|
||||
toml_datum_t authorsDatum = toml_string_in(tomlData, "authors");
|
||||
QString authors = "";
|
||||
if(authorsDatum.ok)
|
||||
{
|
||||
authors = authorsDatum.u.s;
|
||||
free(authorsDatum.u.s);
|
||||
}
|
||||
else
|
||||
{
|
||||
authorsDatum = toml_string_in(tomlModsTable0, "authors");
|
||||
if(authorsDatum.ok)
|
||||
{
|
||||
authors = authorsDatum.u.s;
|
||||
free(authorsDatum.u.s);
|
||||
}
|
||||
}
|
||||
if(!authors.isEmpty())
|
||||
{
|
||||
// author information is stored as a string now, not a list
|
||||
details->authors.append(authors);
|
||||
}
|
||||
// is credits even used anywhere? including this for completion/parity with old data version
|
||||
toml_datum_t creditsDatum = toml_string_in(tomlData, "credits");
|
||||
QString credits = "";
|
||||
if(creditsDatum.ok)
|
||||
{
|
||||
authors = creditsDatum.u.s;
|
||||
free(creditsDatum.u.s);
|
||||
}
|
||||
else
|
||||
{
|
||||
creditsDatum = toml_string_in(tomlModsTable0, "credits");
|
||||
if(creditsDatum.ok)
|
||||
{
|
||||
credits = creditsDatum.u.s;
|
||||
free(creditsDatum.u.s);
|
||||
}
|
||||
}
|
||||
details->credits = credits;
|
||||
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
|
||||
QString homeurl = "";
|
||||
if(homeurlDatum.ok)
|
||||
{
|
||||
homeurl = homeurlDatum.u.s;
|
||||
free(homeurlDatum.u.s);
|
||||
}
|
||||
else
|
||||
{
|
||||
homeurlDatum = toml_string_in(tomlModsTable0, "displayURL");
|
||||
if(homeurlDatum.ok)
|
||||
{
|
||||
homeurl = homeurlDatum.u.s;
|
||||
free(homeurlDatum.u.s);
|
||||
}
|
||||
}
|
||||
if(!homeurl.isEmpty())
|
||||
{
|
||||
// fix up url.
|
||||
if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://"))
|
||||
{
|
||||
homeurl.prepend("http://");
|
||||
}
|
||||
}
|
||||
details->homeurl = homeurl;
|
||||
|
||||
// this seems to be recursive, so it should free everything
|
||||
toml_free(tomlData);
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
// 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("META-INF/mods.toml"))
|
||||
{
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
zip.close();
|
||||
return;
|
||||
}
|
||||
|
||||
m_result->details = ReadMCModTOML(file.readAll());
|
||||
file.close();
|
||||
|
||||
// to replace ${file.jarVersion} with the actual version, as needed
|
||||
if (m_result->details && m_result->details->version == "${file.jarVersion}")
|
||||
{
|
||||
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
|
||||
{
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
zip.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// quick and dirty line-by-line parser
|
||||
auto manifestLines = file.readAll().split('\n');
|
||||
QString manifestVersion = "";
|
||||
for (auto &line : manifestLines)
|
||||
{
|
||||
if (QString(line).startsWith("Implementation-Version: "))
|
||||
{
|
||||
manifestVersion = QString(line).remove("Implementation-Version: ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF
|
||||
// also keep with forge's behavior of setting the version to "NONE" if none is found
|
||||
if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "")
|
||||
{
|
||||
manifestVersion = "NONE";
|
||||
}
|
||||
|
||||
m_result->details->version = manifestVersion;
|
||||
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
zip.close();
|
||||
return;
|
||||
}
|
||||
else 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
launcher/minecraft/mod/LocalModParseTask.h
Normal file
37
launcher/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;
|
||||
};
|
151
launcher/minecraft/mod/Mod.cpp
Normal file
151
launcher/minecraft/mod/Mod.cpp
Normal file
@ -0,0 +1,151 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <QDir>
|
||||
#include <QString>
|
||||
|
||||
#include "Mod.h"
|
||||
#include <QDebug>
|
||||
#include <FileSystem.h>
|
||||
|
||||
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;
|
||||
}
|
||||
repath(QFileInfo(path));
|
||||
m_enabled = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Mod::destroy()
|
||||
{
|
||||
m_type = MOD_UNKNOWN;
|
||||
return FS::deletePath(m_file.filePath());
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
115
launcher/minecraft/mod/Mod.h
Normal file
115
launcher/minecraft/mod/Mod.h
Normal file
@ -0,0 +1,115 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <QFileInfo>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <memory>
|
||||
|
||||
#include "ModDetails.h"
|
||||
|
||||
|
||||
|
||||
class Mod
|
||||
{
|
||||
public:
|
||||
enum ModType
|
||||
{
|
||||
MOD_UNKNOWN, //!< Indicates an unspecified mod type.
|
||||
MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
|
||||
MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
|
||||
MOD_FOLDER, //!< The mod is in a folder on the filesystem.
|
||||
MOD_LITEMOD, //!< The mod is a litemod
|
||||
};
|
||||
|
||||
Mod() = default;
|
||||
Mod(const QFileInfo &file);
|
||||
|
||||
QFileInfo filename() const
|
||||
{
|
||||
return m_file;
|
||||
}
|
||||
QString mmc_id() const
|
||||
{
|
||||
return m_mmc_id;
|
||||
}
|
||||
ModType type() const
|
||||
{
|
||||
return m_type;
|
||||
}
|
||||
bool valid()
|
||||
{
|
||||
return m_type != MOD_UNKNOWN;
|
||||
}
|
||||
|
||||
QDateTime dateTimeChanged() const
|
||||
{
|
||||
return m_changedDateTime;
|
||||
}
|
||||
|
||||
bool enabled() const
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
const ModDetails &details() const;
|
||||
|
||||
QString name() const;
|
||||
QString version() const;
|
||||
QString homeurl() const;
|
||||
QString description() const;
|
||||
QStringList authors() const;
|
||||
|
||||
bool enable(bool value);
|
||||
|
||||
// delete all the files of this mod
|
||||
bool destroy();
|
||||
|
||||
// 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;
|
||||
std::shared_ptr<ModDetails> m_localDetails;
|
||||
};
|
17
launcher/minecraft/mod/ModDetails.h
Normal file
17
launcher/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
launcher/minecraft/mod/ModFolderLoadTask.cpp
Normal file
18
launcher/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
launcher/minecraft/mod/ModFolderLoadTask.h
Normal file
29
launcher/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;
|
||||
};
|
554
launcher/minecraft/mod/ModFolderModel.cpp
Normal file
554
launcher/minecraft/mod/ModFolderModel.cpp
Normal file
@ -0,0 +1,554 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ModFolderModel.h"
|
||||
#include <FileSystem.h>
|
||||
#include <QMimeData>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
#include <QString>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QDebug>
|
||||
#include "ModFolderLoadTask.h"
|
||||
#include <QThreadPool>
|
||||
#include <algorithm>
|
||||
#include "LocalModParseTask.h"
|
||||
|
||||
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);
|
||||
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
|
||||
m_watcher = new QFileSystemWatcher(this);
|
||||
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
|
||||
}
|
||||
|
||||
void ModFolderModel::startWatching()
|
||||
{
|
||||
if(is_watching)
|
||||
return;
|
||||
|
||||
update();
|
||||
|
||||
is_watching = m_watcher->addPath(m_dir.absolutePath());
|
||||
if (is_watching)
|
||||
{
|
||||
qDebug() << "Started watching " << m_dir.absolutePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to start watching " << m_dir.absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
void ModFolderModel::stopWatching()
|
||||
{
|
||||
if(!is_watching)
|
||||
return;
|
||||
|
||||
is_watching = !m_watcher->removePath(m_dir.absolutePath());
|
||||
if (!is_watching)
|
||||
{
|
||||
qDebug() << "Stopped watching " << m_dir.absolutePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
bool ModFolderModel::update()
|
||||
{
|
||||
if (!isValid()) {
|
||||
return false;
|
||||
}
|
||||
if(m_update) {
|
||||
scheduled_update = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto task = new ModFolderLoadTask(m_dir);
|
||||
m_update = task->result();
|
||||
QThreadPool *threadPool = QThreadPool::globalInstance();
|
||||
connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
|
||||
threadPool->start(task);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModFolderModel::finishUpdate()
|
||||
{
|
||||
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(), std::greater<int>());
|
||||
for(auto iter = removedRows.begin(); iter != removedRows.end(); 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 updateFinished();
|
||||
|
||||
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::finishModParse);
|
||||
threadPool->start(task);
|
||||
}
|
||||
|
||||
void ModFolderModel::finishModParse(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;
|
||||
}
|
||||
interaction_disabled = disabled;
|
||||
if(size()) {
|
||||
emit dataChanged(index(0), index(size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
void ModFolderModel::directoryChanged(QString path)
|
||||
{
|
||||
update();
|
||||
}
|
||||
|
||||
bool ModFolderModel::isValid()
|
||||
{
|
||||
return m_dir.exists() && m_dir.isReadable();
|
||||
}
|
||||
|
||||
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
|
||||
bool ModFolderModel::installMod(const QString &filename)
|
||||
{
|
||||
if(interaction_disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
|
||||
auto originalPath = FS::NormalizePath(filename);
|
||||
QFileInfo fileinfo(originalPath);
|
||||
|
||||
if (!fileinfo.exists() || !fileinfo.isReadable())
|
||||
{
|
||||
qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
|
||||
return false;
|
||||
}
|
||||
qDebug() << "installing: " << fileinfo.absoluteFilePath();
|
||||
|
||||
Mod installedMod(fileinfo);
|
||||
if (!installedMod.valid())
|
||||
{
|
||||
qDebug() << originalPath << "is not a valid mod. Ignoring it.";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto type = installedMod.type();
|
||||
if (type == Mod::MOD_UNKNOWN)
|
||||
{
|
||||
qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
|
||||
if(originalPath == newpath)
|
||||
{
|
||||
qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
|
||||
{
|
||||
if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
|
||||
{
|
||||
if(!QFile::remove(newpath))
|
||||
{
|
||||
// FIXME: report error in a user-visible way
|
||||
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
|
||||
return false;
|
||||
}
|
||||
qDebug() << newpath << "has been deleted.";
|
||||
}
|
||||
if (!QFile::copy(fileinfo.filePath(), newpath))
|
||||
{
|
||||
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
|
||||
// FIXME: report error in a user-visible way
|
||||
return false;
|
||||
}
|
||||
FS::updateTimestamp(newpath);
|
||||
installedMod.repath(newpath);
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
else if (type == Mod::MOD_FOLDER)
|
||||
{
|
||||
QString from = fileinfo.filePath();
|
||||
if(QFile::exists(newpath))
|
||||
{
|
||||
qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FS::copy(from, newpath)())
|
||||
{
|
||||
qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
|
||||
return false;
|
||||
}
|
||||
installedMod.repath(newpath);
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
|
||||
{
|
||||
if(interaction_disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(indexes.isEmpty())
|
||||
return true;
|
||||
|
||||
for (auto index: indexes)
|
||||
{
|
||||
if(index.column() != 0) {
|
||||
continue;
|
||||
}
|
||||
setModStatus(index.row(), enable);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
|
||||
{
|
||||
if(interaction_disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(indexes.isEmpty())
|
||||
return true;
|
||||
|
||||
for (auto i: indexes)
|
||||
{
|
||||
Mod &m = mods[i.row()];
|
||||
m.destroy();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int ModFolderModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return NUM_COLUMNS;
|
||||
}
|
||||
|
||||
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
int row = index.row();
|
||||
int column = index.column();
|
||||
|
||||
if (row < 0 || row >= mods.size())
|
||||
return QVariant();
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
switch (column)
|
||||
{
|
||||
case NameColumn:
|
||||
return mods[row].name();
|
||||
case VersionColumn: {
|
||||
switch(mods[row].type()) {
|
||||
case Mod::MOD_FOLDER:
|
||||
return tr("Folder");
|
||||
case Mod::MOD_SINGLEFILE:
|
||||
return tr("File");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return mods[row].version();
|
||||
}
|
||||
case DateColumn:
|
||||
return mods[row].dateTimeChanged();
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
return mods[row].mmc_id();
|
||||
|
||||
case Qt::CheckStateRole:
|
||||
switch (column)
|
||||
{
|
||||
case ActiveColumn:
|
||||
return mods[row].enabled() ? Qt::Checked : Qt::Unchecked;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == Qt::CheckStateRole)
|
||||
{
|
||||
return setModStatus(index.row(), Toggle);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
|
||||
{
|
||||
if(row < 0 || row >= mods.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &mod = mods[row];
|
||||
bool desiredStatus;
|
||||
switch(action) {
|
||||
case Enable:
|
||||
desiredStatus = true;
|
||||
break;
|
||||
case Disable:
|
||||
desiredStatus = false;
|
||||
break;
|
||||
case Toggle:
|
||||
default:
|
||||
desiredStatus = !mod.enabled();
|
||||
break;
|
||||
}
|
||||
|
||||
if(desiredStatus == mod.enabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// preserve the row, but change its ID
|
||||
auto oldId = mod.mmc_id();
|
||||
if(!mod.enable(!mod.enabled())) {
|
||||
return false;
|
||||
}
|
||||
auto newId = mod.mmc_id();
|
||||
if(modsIndex.contains(newId)) {
|
||||
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
|
||||
// But is it necessary?
|
||||
}
|
||||
modsIndex.remove(oldId);
|
||||
modsIndex[newId] = row;
|
||||
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
switch (section)
|
||||
{
|
||||
case ActiveColumn:
|
||||
return QString();
|
||||
case NameColumn:
|
||||
return tr("Name");
|
||||
case VersionColumn:
|
||||
return tr("Version");
|
||||
case DateColumn:
|
||||
return tr("Last changed");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
switch (section)
|
||||
{
|
||||
case ActiveColumn:
|
||||
return tr("Is the mod enabled?");
|
||||
case NameColumn:
|
||||
return tr("The name of the mod.");
|
||||
case VersionColumn:
|
||||
return tr("The version of the mod.");
|
||||
case DateColumn:
|
||||
return tr("The date and time this mod was last changed (or added).");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
|
||||
auto flags = defaultFlags;
|
||||
if(interaction_disabled) {
|
||||
flags &= ~Qt::ItemIsDropEnabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
flags |= Qt::ItemIsDropEnabled;
|
||||
if(index.isValid()) {
|
||||
flags |= Qt::ItemIsUserCheckable;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
Qt::DropActions ModFolderModel::supportedDropActions() const
|
||||
{
|
||||
// copy from outside, move from within and other mod lists
|
||||
return Qt::CopyAction | Qt::MoveAction;
|
||||
}
|
||||
|
||||
QStringList ModFolderModel::mimeTypes() const
|
||||
{
|
||||
QStringList types;
|
||||
types << "text/uri-list";
|
||||
return types;
|
||||
}
|
||||
|
||||
bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
|
||||
{
|
||||
if (action == Qt::IgnoreAction)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if the action is supported
|
||||
if (!data || !(action & supportedDropActions()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// files dropped from outside?
|
||||
if (data->hasUrls())
|
||||
{
|
||||
auto urls = data->urls();
|
||||
for (auto url : urls)
|
||||
{
|
||||
// only local files may be dropped...
|
||||
if (!url.isLocalFile())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// TODO: implement not only copy, but also move
|
||||
// FIXME: handle errors here
|
||||
installMod(url.toLocalFile());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
148
launcher/minecraft/mod/ModFolderModel.h
Normal file
148
launcher/minecraft/mod/ModFolderModel.h
Normal file
@ -0,0 +1,148 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include "Mod.h"
|
||||
|
||||
#include "ModFolderLoadTask.h"
|
||||
#include "LocalModParseTask.h"
|
||||
|
||||
class LegacyInstance;
|
||||
class BaseInstance;
|
||||
class QFileSystemWatcher;
|
||||
|
||||
/**
|
||||
* A legacy mod list.
|
||||
* Backed by a folder.
|
||||
*/
|
||||
class ModFolderModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Columns
|
||||
{
|
||||
ActiveColumn = 0,
|
||||
NameColumn,
|
||||
VersionColumn,
|
||||
DateColumn,
|
||||
NUM_COLUMNS
|
||||
};
|
||||
enum ModStatusAction {
|
||||
Disable,
|
||||
Enable,
|
||||
Toggle
|
||||
};
|
||||
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;
|
||||
Qt::DropActions supportedDropActions() const override;
|
||||
|
||||
/// flags, mostly to support drag&drop
|
||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QStringList mimeTypes() const override;
|
||||
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
|
||||
|
||||
virtual int rowCount(const QModelIndex &) const override
|
||||
{
|
||||
return size();
|
||||
}
|
||||
|
||||
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
virtual int columnCount(const QModelIndex &parent) const override;
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return mods.size();
|
||||
}
|
||||
;
|
||||
bool empty() const
|
||||
{
|
||||
return size() == 0;
|
||||
}
|
||||
Mod &operator[](size_t index)
|
||||
{
|
||||
return mods[index];
|
||||
}
|
||||
const Mod &at(size_t index) const
|
||||
{
|
||||
return mods.at(index);
|
||||
}
|
||||
|
||||
/// Reloads the mod list and returns true if the list changed.
|
||||
bool update();
|
||||
|
||||
/**
|
||||
* Adds the given mod to the list at the given index - if the list supports custom ordering
|
||||
*/
|
||||
bool installMod(const QString& filename);
|
||||
|
||||
/// Deletes all the selected mods
|
||||
bool deleteMods(const QModelIndexList &indexes);
|
||||
|
||||
/// Enable or disable listed mods
|
||||
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
|
||||
|
||||
void startWatching();
|
||||
void stopWatching();
|
||||
|
||||
bool isValid();
|
||||
|
||||
QDir dir()
|
||||
{
|
||||
return m_dir;
|
||||
}
|
||||
|
||||
const QList<Mod> & allMods()
|
||||
{
|
||||
return mods;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void disableInteraction(bool disabled);
|
||||
|
||||
private
|
||||
slots:
|
||||
void directoryChanged(QString path);
|
||||
void finishUpdate();
|
||||
void finishModParse(int token);
|
||||
|
||||
signals:
|
||||
void updateFinished();
|
||||
|
||||
private:
|
||||
void resolveMod(Mod& m);
|
||||
bool setModStatus(int index, ModStatusAction action);
|
||||
|
||||
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;
|
||||
};
|
53
launcher/minecraft/mod/ModFolderModel_test.cpp
Normal file
53
launcher/minecraft/mod/ModFolderModel_test.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
#include <QTest>
|
||||
#include <QTemporaryDir>
|
||||
#include "TestUtil.h"
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
|
||||
class ModFolderModelTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private
|
||||
slots:
|
||||
// test for GH-1178 - install a folder with files to a mod list
|
||||
void test_1178()
|
||||
{
|
||||
// source
|
||||
QString source = QFINDTESTDATA("data/test_folder");
|
||||
|
||||
// sanity check
|
||||
QVERIFY(!source.endsWith('/'));
|
||||
|
||||
auto verify = [](QString path)
|
||||
{
|
||||
QDir target_dir(FS::PathCombine(path, "test_folder"));
|
||||
QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
|
||||
QVERIFY(target_dir.entryList().contains("assets"));
|
||||
};
|
||||
|
||||
// 1. test with no trailing /
|
||||
{
|
||||
QString folder = source;
|
||||
QTemporaryDir tempDir;
|
||||
ModFolderModel m(tempDir.path());
|
||||
m.installMod(folder);
|
||||
verify(tempDir.path());
|
||||
}
|
||||
|
||||
// 2. test with trailing /
|
||||
{
|
||||
QString folder = source + '/';
|
||||
QTemporaryDir tempDir;
|
||||
ModFolderModel m(tempDir.path());
|
||||
m.installMod(folder);
|
||||
verify(tempDir.path());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(ModFolderModelTest)
|
||||
|
||||
#include "ModFolderModel_test.moc"
|
23
launcher/minecraft/mod/ResourcePackFolderModel.cpp
Normal file
23
launcher/minecraft/mod/ResourcePackFolderModel.cpp
Normal file
@ -0,0 +1,23 @@
|
||||
#include "ResourcePackFolderModel.h"
|
||||
|
||||
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
|
||||
}
|
||||
|
||||
QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (role == Qt::ToolTipRole) {
|
||||
switch (section) {
|
||||
case ActiveColumn:
|
||||
return tr("Is the resource pack enabled?");
|
||||
case NameColumn:
|
||||
return tr("The name of the resource pack.");
|
||||
case VersionColumn:
|
||||
return tr("The version of the resource pack.");
|
||||
case DateColumn:
|
||||
return tr("The date and time this resource pack was last changed (or added).");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
return ModFolderModel::headerData(section, orientation, role);
|
||||
}
|
13
launcher/minecraft/mod/ResourcePackFolderModel.h
Normal file
13
launcher/minecraft/mod/ResourcePackFolderModel.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModFolderModel.h"
|
||||
|
||||
class ResourcePackFolderModel : public ModFolderModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ResourcePackFolderModel(const QString &dir);
|
||||
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
};
|
23
launcher/minecraft/mod/TexturePackFolderModel.cpp
Normal file
23
launcher/minecraft/mod/TexturePackFolderModel.cpp
Normal file
@ -0,0 +1,23 @@
|
||||
#include "TexturePackFolderModel.h"
|
||||
|
||||
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
|
||||
}
|
||||
|
||||
QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (role == Qt::ToolTipRole) {
|
||||
switch (section) {
|
||||
case ActiveColumn:
|
||||
return tr("Is the texture pack enabled?");
|
||||
case NameColumn:
|
||||
return tr("The name of the texture pack.");
|
||||
case VersionColumn:
|
||||
return tr("The version of the texture pack.");
|
||||
case DateColumn:
|
||||
return tr("The date and time this texture pack was last changed (or added).");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
return ModFolderModel::headerData(section, orientation, role);
|
||||
}
|
13
launcher/minecraft/mod/TexturePackFolderModel.h
Normal file
13
launcher/minecraft/mod/TexturePackFolderModel.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModFolderModel.h"
|
||||
|
||||
class TexturePackFolderModel : public ModFolderModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TexturePackFolderModel(const QString &dir);
|
||||
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
};
|
Reference in New Issue
Block a user