mirror of
https://github.com/MultiMC/MultiMC5.git
synced 2024-12-27 03:14:49 +00:00
538 lines
16 KiB
C++
538 lines
16 KiB
C++
#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");
|
|
if(!tomlModsArr)
|
|
{
|
|
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
|
|
return nullptr;
|
|
}
|
|
|
|
// 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);
|
|
if(!tomlModsTable0)
|
|
{
|
|
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
|
|
return nullptr;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
|
|
std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
|
|
{
|
|
QJsonParseError jsonError;
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
|
auto object = jsonDoc.object();
|
|
|
|
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
|
|
|
if (object.contains("schema_version") && object.value("schema_version").toInt() == 1)
|
|
{
|
|
QJsonObject loader = object.value("quilt_loader").toObject();
|
|
details->mod_id = loader.value("id").toString();
|
|
details->version = loader.value("version").toString();
|
|
|
|
if (loader.contains("metadata"))
|
|
{
|
|
QJsonObject metadata = loader.value("metadata").toObject();
|
|
details->name = metadata.contains("name") ? metadata.value("name").toString() : details->mod_id;
|
|
|
|
if (metadata.contains("description"))
|
|
{
|
|
details->description = metadata.value("description").toString();
|
|
}
|
|
|
|
if (metadata.contains("contributors"))
|
|
{
|
|
// NOTE: This lists every contributor, not just "authors"
|
|
details->authors = metadata.value("contributors").toObject().keys();
|
|
}
|
|
|
|
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("quilt.mod.json"))
|
|
{
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
{
|
|
zip.close();
|
|
return;
|
|
}
|
|
|
|
m_result->details = ReadQuiltModInfo(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);
|
|
}
|