mirror of
https://github.com/MultiMC/MultiMC5.git
synced 2024-12-28 15:18:21 +00:00
490 lines
18 KiB
C++
490 lines
18 KiB
C++
/* Copyright 2013-2024 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 "InstanceImportTask.h"
|
|
#include "BaseInstance.h"
|
|
#include "FileSystem.h"
|
|
#include "Application.h"
|
|
#include "MMCZip.h"
|
|
#include "NullInstance.h"
|
|
#include "settings/INISettingsObject.h"
|
|
#include "icons/IconUtils.h"
|
|
#include <QtConcurrentRun>
|
|
|
|
// FIXME: this does not belong here, it's Minecraft/Flame specific
|
|
#include "minecraft/MinecraftInstance.h"
|
|
#include "minecraft/PackProfile.h"
|
|
#include "Json.h"
|
|
#include <quazipdir.h>
|
|
#include "modplatform/modrinth/ModrinthPackManifest.h"
|
|
#include "modplatform/technic/TechnicPackProcessor.h"
|
|
|
|
#include "icons/IconList.h"
|
|
#include "Application.h"
|
|
#include "net/ChecksumValidator.h"
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
|
|
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
|
|
{
|
|
m_sourceUrl = sourceUrl;
|
|
}
|
|
|
|
void InstanceImportTask::executeTask()
|
|
{
|
|
if (m_sourceUrl.isLocalFile())
|
|
{
|
|
m_archivePath = m_sourceUrl.toLocalFile();
|
|
processZipPack();
|
|
}
|
|
else
|
|
{
|
|
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
|
|
m_downloadRequired = true;
|
|
|
|
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
|
|
auto entry = APPLICATION->metacache()->resolveEntry("general", path);
|
|
entry->setStale(true);
|
|
m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
|
|
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
|
|
m_archivePath = entry->getFullPath();
|
|
auto job = m_filesNetJob.get();
|
|
connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
|
|
connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
|
|
connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed);
|
|
m_filesNetJob->start();
|
|
}
|
|
}
|
|
|
|
void InstanceImportTask::downloadSucceeded()
|
|
{
|
|
processZipPack();
|
|
m_filesNetJob.reset();
|
|
}
|
|
|
|
void InstanceImportTask::downloadFailed(QString reason)
|
|
{
|
|
emitFailed(reason);
|
|
m_filesNetJob.reset();
|
|
}
|
|
|
|
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
|
|
{
|
|
setProgress(current / 2, total);
|
|
}
|
|
|
|
void InstanceImportTask::processZipPack()
|
|
{
|
|
setStatus(tr("Extracting modpack"));
|
|
QDir extractDir(m_stagingPath);
|
|
qDebug() << "Attempting to create instance from" << m_archivePath;
|
|
|
|
// open the zip and find relevant files in it
|
|
m_packZip.reset(new QuaZip(m_archivePath));
|
|
if (!m_packZip->open(QuaZip::mdUnzip))
|
|
{
|
|
emitFailed(tr("Unable to open supplied modpack zip file."));
|
|
return;
|
|
}
|
|
|
|
QStringList blacklist = {"instance.cfg", "manifest.json"};
|
|
QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
|
|
bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json");
|
|
QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json");
|
|
QString root;
|
|
if(!mmcFound.isNull())
|
|
{
|
|
// process as MultiMC instance/pack
|
|
qDebug() << "MultiMC:" << mmcFound;
|
|
root = mmcFound;
|
|
m_modpackType = ModpackType::MultiMC;
|
|
}
|
|
else if (technicFound)
|
|
{
|
|
// process as Technic pack
|
|
qDebug() << "Technic:" << technicFound;
|
|
extractDir.mkpath(".minecraft");
|
|
extractDir.cd(".minecraft");
|
|
m_modpackType = ModpackType::Technic;
|
|
}
|
|
else if(!modrinthFound.isNull())
|
|
{
|
|
// process as Modrinth pack
|
|
qDebug() << "Modrinth:" << modrinthFound;
|
|
root = modrinthFound;
|
|
m_modpackType = ModpackType::Modrinth;
|
|
}
|
|
if(m_modpackType == ModpackType::Unknown)
|
|
{
|
|
emitFailed(tr("Archive does not contain a recognized modpack type."));
|
|
return;
|
|
}
|
|
|
|
// make sure we extract just the pack
|
|
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
|
|
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished);
|
|
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &InstanceImportTask::extractAborted);
|
|
m_extractFutureWatcher.setFuture(m_extractFuture);
|
|
}
|
|
|
|
void InstanceImportTask::extractFinished()
|
|
{
|
|
m_packZip.reset();
|
|
if (!m_extractFuture.result())
|
|
{
|
|
emitFailed(tr("Failed to extract modpack"));
|
|
return;
|
|
}
|
|
QDir extractDir(m_stagingPath);
|
|
|
|
qDebug() << "Fixing permissions for extracted pack files...";
|
|
QDirIterator it(extractDir, QDirIterator::Subdirectories);
|
|
while (it.hasNext())
|
|
{
|
|
auto filepath = it.next();
|
|
QFileInfo file(filepath);
|
|
auto permissions = QFile::permissions(filepath);
|
|
auto origPermissions = permissions;
|
|
if(file.isDir())
|
|
{
|
|
// Folder +rwx for current user
|
|
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
|
|
}
|
|
else
|
|
{
|
|
// File +rw for current user
|
|
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
|
|
}
|
|
if(origPermissions != permissions)
|
|
{
|
|
if(!QFile::setPermissions(filepath, permissions))
|
|
{
|
|
logWarning(tr("Could not fix permissions for %1").arg(filepath));
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "Fixed" << filepath;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch(m_modpackType)
|
|
{
|
|
case ModpackType::MultiMC:
|
|
processMultiMC();
|
|
return;
|
|
case ModpackType::Technic:
|
|
processTechnic();
|
|
return;
|
|
case ModpackType::Modrinth:
|
|
processModrinth();
|
|
return;
|
|
case ModpackType::Unknown:
|
|
emitFailed(tr("Archive does not contain a recognized modpack type."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
void InstanceImportTask::extractAborted()
|
|
{
|
|
emitFailed(tr("Instance import has been aborted."));
|
|
return;
|
|
}
|
|
|
|
void InstanceImportTask::processTechnic()
|
|
{
|
|
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
|
|
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
|
|
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
|
|
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath);
|
|
}
|
|
|
|
void InstanceImportTask::processMultiMC()
|
|
{
|
|
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
|
|
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
|
|
instanceSettings->registerSetting("InstanceType", "Legacy");
|
|
|
|
NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
|
|
|
|
// reset time played on import... because packs.
|
|
instance.resetTimePlayed();
|
|
|
|
// set a new nice name
|
|
instance.setName(m_instName);
|
|
|
|
// if the icon was specified by user, use that. otherwise pull icon from the pack
|
|
if (m_instIcon != "default")
|
|
{
|
|
instance.setIconKey(m_instIcon);
|
|
}
|
|
else
|
|
{
|
|
m_instIcon = instance.iconKey();
|
|
|
|
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
|
|
if (!importIconPath.isNull() && QFile::exists(importIconPath))
|
|
{
|
|
// import icon
|
|
auto iconList = APPLICATION->icons();
|
|
if (iconList->iconFileExists(m_instIcon))
|
|
{
|
|
iconList->deleteIcon(m_instIcon);
|
|
}
|
|
iconList->installIcons({importIconPath});
|
|
}
|
|
}
|
|
emitSucceeded();
|
|
}
|
|
|
|
namespace {
|
|
bool mergeOverrides(const QString &fromDir, const QString &toDir) {
|
|
QDir dir(fromDir);
|
|
if(!dir.exists()) {
|
|
return true;
|
|
}
|
|
if(!FS::ensureFolderPathExists(toDir)) {
|
|
return false;
|
|
}
|
|
const int absSourcePathLength = dir.absoluteFilePath(fromDir).length();
|
|
|
|
QDirIterator it(fromDir, QDirIterator::Subdirectories);
|
|
while (it.hasNext()){
|
|
it.next();
|
|
const auto fileInfo = it.fileInfo();
|
|
auto fileName = fileInfo.fileName();
|
|
if(fileName == "." || fileName == "..") {
|
|
continue;
|
|
}
|
|
const QString subPathStructure = fileInfo.absoluteFilePath().mid(absSourcePathLength);
|
|
const QString constructedAbsolutePath = toDir + subPathStructure;
|
|
|
|
if(fileInfo.isDir()){
|
|
//Create directory in target folder
|
|
dir.mkpath(constructedAbsolutePath);
|
|
} else if(fileInfo.isFile()) {
|
|
QFileInfo targetFileInfo(constructedAbsolutePath);
|
|
if(targetFileInfo.exists()) {
|
|
continue;
|
|
}
|
|
// move
|
|
QFile::rename(fileInfo.absoluteFilePath(), constructedAbsolutePath);
|
|
}
|
|
}
|
|
|
|
dir.removeRecursively();
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
void InstanceImportTask::processModrinth() {
|
|
std::vector<Modrinth::File> files;
|
|
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion, neoforgeVersion;
|
|
try
|
|
{
|
|
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
|
|
auto doc = Json::requireDocument(indexPath);
|
|
auto obj = Json::requireObject(doc, "modrinth.index.json");
|
|
int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
|
|
if (formatVersion == 1)
|
|
{
|
|
auto game = Json::requireString(obj, "game", "modrinth.index.json");
|
|
if (game != "minecraft")
|
|
{
|
|
throw JSONValidationError("Unknown game: " + game);
|
|
}
|
|
|
|
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
|
|
for(auto & obj: jsonFiles) {
|
|
Modrinth::File file;
|
|
auto dirtyPath = Json::requireString(obj, "path");
|
|
dirtyPath.replace('\\', '/');
|
|
auto simplifiedPath = QDir::cleanPath(dirtyPath);
|
|
QFileInfo fileInfo (simplifiedPath);
|
|
if(simplifiedPath.startsWith("../") || simplifiedPath.contains("/../") || fileInfo.isAbsolute()) {
|
|
throw JSONValidationError("Invalid path found in modpack files:\n\n" + simplifiedPath);
|
|
}
|
|
file.path = simplifiedPath;
|
|
|
|
// env doesn't have to be present, in that case mod is required
|
|
auto env = Json::ensureObject(obj, "env");
|
|
auto clientEnv = Json::ensureString(env, "client", "required");
|
|
|
|
if(clientEnv == "required") {
|
|
// NOOP
|
|
}
|
|
else if(clientEnv == "optional") {
|
|
file.path += ".disabled";
|
|
}
|
|
else if(clientEnv == "unsupported") {
|
|
continue;
|
|
}
|
|
|
|
QJsonObject hashes = Json::requireObject(obj, "hashes");
|
|
QString hash;
|
|
QCryptographicHash::Algorithm hashAlgorithm;
|
|
hash = Json::ensureString(hashes, "sha256");
|
|
hashAlgorithm = QCryptographicHash::Sha256;
|
|
if (hash.isEmpty())
|
|
{
|
|
hash = Json::ensureString(hashes, "sha512");
|
|
hashAlgorithm = QCryptographicHash::Sha512;
|
|
if (hash.isEmpty())
|
|
{
|
|
hash = Json::ensureString(hashes, "sha1");
|
|
hashAlgorithm = QCryptographicHash::Sha1;
|
|
if (hash.isEmpty())
|
|
{
|
|
throw JSONValidationError("No hash found for: " + file.path);
|
|
}
|
|
}
|
|
}
|
|
file.hash = QByteArray::fromHex(hash.toLatin1());
|
|
file.hashAlgorithm = hashAlgorithm;
|
|
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces)
|
|
file.download = Json::requireValueString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path);
|
|
if (!file.download.isValid())
|
|
{
|
|
throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
|
|
}
|
|
files.push_back(file);
|
|
}
|
|
|
|
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
|
|
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it)
|
|
{
|
|
QString name = it.key();
|
|
if (name == "minecraft")
|
|
{
|
|
if (!minecraftVersion.isEmpty())
|
|
throw JSONValidationError("Duplicate Minecraft version");
|
|
minecraftVersion = Json::requireValueString(*it, "Minecraft version");
|
|
}
|
|
else if (name == "fabric-loader")
|
|
{
|
|
if (!fabricVersion.isEmpty())
|
|
throw JSONValidationError("Duplicate Fabric Loader version");
|
|
fabricVersion = Json::requireValueString(*it, "Fabric Loader version");
|
|
}
|
|
else if (name == "quilt-loader")
|
|
{
|
|
if (!quiltVersion.isEmpty())
|
|
throw JSONValidationError("Duplicate Quilt Loader version");
|
|
quiltVersion = Json::requireValueString(*it, "Quilt Loader version");
|
|
}
|
|
else if (name == "forge")
|
|
{
|
|
if (!forgeVersion.isEmpty())
|
|
throw JSONValidationError("Duplicate Forge version");
|
|
forgeVersion = Json::requireValueString(*it, "Forge version");
|
|
}
|
|
else if (name == "neoforge")
|
|
{
|
|
if (!neoforgeVersion.isEmpty())
|
|
throw JSONValidationError("Duplicate NeoForge version");
|
|
neoforgeVersion = Json::requireValueString(*it, "NeoForge version");
|
|
}
|
|
else
|
|
{
|
|
throw JSONValidationError("Unknown dependency type: " + name);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion));
|
|
}
|
|
QFile::remove(indexPath);
|
|
}
|
|
catch (const JSONValidationError &e)
|
|
{
|
|
emitFailed(tr("Could not understand pack index:\n") + e.cause());
|
|
return;
|
|
}
|
|
QString clientOverridePath = FS::PathCombine(m_stagingPath, "client-overrides");
|
|
if (QFile::exists(clientOverridePath)) {
|
|
QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft");
|
|
if (!QFile::rename(clientOverridePath, mcPath)) {
|
|
emitFailed(tr("Could not rename the overrides folder:\n") + "overrides");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// TODO: only extract things we actually want instead of everything only to just delete it afterwards ...
|
|
if(!mergeOverrides(FS::PathCombine(m_stagingPath, "client-overrides"), FS::PathCombine(m_stagingPath, ".minecraft"))) {
|
|
emitFailed(tr("Could not merge the overrides folder:\n") + "client-overrides");
|
|
return;
|
|
}
|
|
|
|
if(!mergeOverrides(FS::PathCombine(m_stagingPath, "overrides"), FS::PathCombine(m_stagingPath, ".minecraft"))) {
|
|
emitFailed(tr("Could not merge the overrides folder:\n") + "overrides");
|
|
return;
|
|
}
|
|
|
|
FS::deletePath(FS::PathCombine(m_stagingPath, "server-overrides"));
|
|
|
|
|
|
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
|
|
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
|
|
instanceSettings->registerSetting("InstanceType", "Legacy");
|
|
instanceSettings->set("InstanceType", "OneSix");
|
|
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
|
|
auto components = instance.getPackProfile();
|
|
components->buildingFromScratch();
|
|
components->setComponentVersion("net.minecraft", minecraftVersion, true);
|
|
if (!fabricVersion.isEmpty())
|
|
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true);
|
|
if (!quiltVersion.isEmpty())
|
|
components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true);
|
|
if (!forgeVersion.isEmpty())
|
|
components->setComponentVersion("net.minecraftforge", forgeVersion, true);
|
|
if (!neoforgeVersion.isEmpty())
|
|
components->setComponentVersion("net.neoforged", neoforgeVersion, true);
|
|
if (m_instIcon != "default")
|
|
{
|
|
instance.setIconKey(m_instIcon);
|
|
}
|
|
instance.setName(m_instName);
|
|
instance.saveNow();
|
|
|
|
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
|
|
for (auto &file : files)
|
|
{
|
|
auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path);
|
|
qDebug() << "Will download" << file.download << "to" << path;
|
|
auto dl = Net::Download::makeFile(file.download, path);
|
|
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
|
|
m_filesNetJob->addNetAction(dl);
|
|
}
|
|
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
|
|
{
|
|
m_filesNetJob.reset();
|
|
emitSucceeded();
|
|
});
|
|
connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason)
|
|
{
|
|
m_filesNetJob.reset();
|
|
emitFailed(reason);
|
|
});
|
|
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
|
|
{
|
|
setProgress(current, total);
|
|
});
|
|
setStatus(tr("Downloading mods..."));
|
|
m_filesNetJob->start();
|
|
}
|