mirror of
https://github.com/UltimMC/Launcher.git
synced 2025-12-24 12:32:42 +00:00
@@ -306,6 +306,9 @@ set(MINECRAFT_SOURCES
|
||||
# Skin upload utilities
|
||||
minecraft/SkinUpload.cpp
|
||||
minecraft/SkinUpload.h
|
||||
|
||||
mojang/PackageManifest.h
|
||||
mojang/PackageManifest.cpp
|
||||
)
|
||||
|
||||
add_unit_test(GradleSpecifier
|
||||
@@ -313,6 +316,22 @@ add_unit_test(GradleSpecifier
|
||||
LIBS MultiMC_logic
|
||||
)
|
||||
|
||||
add_executable(PackageManifest
|
||||
mojang/PackageManifest_test.cpp
|
||||
)
|
||||
target_link_libraries(PackageManifest
|
||||
MultiMC_logic
|
||||
Qt5::Test
|
||||
)
|
||||
target_include_directories(PackageManifest
|
||||
PRIVATE ../../cmake/UnitTest/
|
||||
)
|
||||
add_test(
|
||||
NAME PackageManifest
|
||||
COMMAND PackageManifest
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
add_unit_test(MojangVersionFormat
|
||||
SOURCES minecraft/MojangVersionFormat_test.cpp
|
||||
LIBS MultiMC_logic
|
||||
@@ -458,6 +477,15 @@ set(MODPACKSCH_SOURCES
|
||||
modplatform/modpacksch/FTBPackManifest.cpp
|
||||
)
|
||||
|
||||
set(TECHNIC_SOURCES
|
||||
modplatform/technic/SingleZipPackInstallTask.h
|
||||
modplatform/technic/SingleZipPackInstallTask.cpp
|
||||
modplatform/technic/SolderPackInstallTask.h
|
||||
modplatform/technic/SolderPackInstallTask.cpp
|
||||
modplatform/technic/TechnicPackProcessor.h
|
||||
modplatform/technic/TechnicPackProcessor.cpp
|
||||
)
|
||||
|
||||
add_unit_test(Index
|
||||
SOURCES meta/Index_test.cpp
|
||||
LIBS MultiMC_logic
|
||||
@@ -489,6 +517,7 @@ set(LOGIC_SOURCES
|
||||
${FTB_SOURCES}
|
||||
${FLAME_SOURCES}
|
||||
${MODPACKSCH_SOURCES}
|
||||
${TECHNIC_SOURCES}
|
||||
)
|
||||
|
||||
add_library(MultiMC_logic SHARED ${LOGIC_SOURCES})
|
||||
|
||||
@@ -98,6 +98,7 @@ void Env::initHttpMetaCache()
|
||||
m_metacache->addBase("general", QDir("cache").absolutePath());
|
||||
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
|
||||
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
|
||||
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
|
||||
m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath());
|
||||
m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
|
||||
m_metacache->addBase("root", QDir::currentPath());
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/* Copyright 2013-2020 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"
|
||||
@@ -15,6 +30,8 @@
|
||||
#include "modplatform/flame/FileResolvingTask.h"
|
||||
#include "modplatform/flame/PackManifest.h"
|
||||
#include "Json.h"
|
||||
#include <quazipdir.h>
|
||||
#include "modplatform/technic/TechnicPackProcessor.h"
|
||||
|
||||
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
|
||||
{
|
||||
@@ -23,8 +40,6 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
|
||||
|
||||
void InstanceImportTask::executeTask()
|
||||
{
|
||||
InstancePtr newInstance;
|
||||
|
||||
if (m_sourceUrl.isLocalFile())
|
||||
{
|
||||
m_archivePath = m_sourceUrl.toLocalFile();
|
||||
@@ -82,6 +97,7 @@ void InstanceImportTask::processZipPack()
|
||||
|
||||
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 flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
|
||||
QString root;
|
||||
if(!mmcFound.isNull())
|
||||
@@ -91,6 +107,14 @@ void InstanceImportTask::processZipPack()
|
||||
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(!flameFound.isNull())
|
||||
{
|
||||
// process as Flame pack
|
||||
@@ -98,7 +122,6 @@ void InstanceImportTask::processZipPack()
|
||||
root = flameFound;
|
||||
m_modpackType = ModpackType::Flame;
|
||||
}
|
||||
|
||||
if(m_modpackType == ModpackType::Unknown)
|
||||
{
|
||||
emitFailed(tr("Archive does not contain a recognized modpack type."));
|
||||
@@ -161,6 +184,9 @@ void InstanceImportTask::extractFinished()
|
||||
case ModpackType::MultiMC:
|
||||
processMultiMC();
|
||||
return;
|
||||
case ModpackType::Technic:
|
||||
processTechnic();
|
||||
return;
|
||||
case ModpackType::Unknown:
|
||||
emitFailed(tr("Archive does not contain a recognized modpack type."));
|
||||
return;
|
||||
@@ -371,6 +397,14 @@ void InstanceImportTask::processFlame()
|
||||
m_modIdResolver->start();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
// FIXME: copy from FolderInstanceProvider!!! FIX IT!!!
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/* Copyright 2013-2020 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 "InstanceTask.h"
|
||||
@@ -29,6 +44,7 @@ private:
|
||||
void processZipPack();
|
||||
void processMultiMC();
|
||||
void processFlame();
|
||||
void processTechnic();
|
||||
|
||||
private slots:
|
||||
void downloadSucceeded();
|
||||
@@ -49,6 +65,7 @@ private: /* data */
|
||||
enum class ModpackType{
|
||||
Unknown,
|
||||
MultiMC,
|
||||
Flame
|
||||
Flame,
|
||||
Technic
|
||||
} m_modpackType = ModpackType::Unknown;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright 2013-2019 MultiMC Contributors
|
||||
/* Copyright 2013-2020 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright 2013-2019 MultiMC Contributors
|
||||
/* Copyright 2013-2020 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -67,5 +67,4 @@ namespace MMCZip
|
||||
* \return The list of the full paths of the files extracted, empty on failure.
|
||||
*/
|
||||
QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir);
|
||||
|
||||
}
|
||||
|
||||
@@ -123,16 +123,20 @@ void PackInstallTask::install()
|
||||
auto dl = Net::Download::makeFile(file.url, path);
|
||||
jobPtr->addNetAction(dl);
|
||||
}
|
||||
|
||||
connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
|
||||
{
|
||||
jobPtr.reset();
|
||||
emitSucceeded();
|
||||
});
|
||||
|
||||
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
|
||||
{
|
||||
jobPtr.reset();
|
||||
emitFailed(reason);
|
||||
|
||||
// FIXME: Temporarily ignore file download failures (matching FTB's installer),
|
||||
// while FTB's data is fucked.
|
||||
qWarning() << "Failed to download files for modpack: " + reason;
|
||||
emitSucceeded();
|
||||
});
|
||||
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
|
||||
{
|
||||
|
||||
129
api/logic/modplatform/technic/SingleZipPackInstallTask.cpp
Normal file
129
api/logic/modplatform/technic/SingleZipPackInstallTask.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
/* Copyright 2013-2020 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 "SingleZipPackInstallTask.h"
|
||||
|
||||
#include "Env.h"
|
||||
#include "MMCZip.h"
|
||||
#include "TechnicPackProcessor.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
#include <FileSystem.h>
|
||||
|
||||
Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
|
||||
{
|
||||
m_sourceUrl = sourceUrl;
|
||||
m_minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
void Technic::SingleZipPackInstallTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
|
||||
|
||||
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
|
||||
auto entry = ENV.metacache()->resolveEntry("general", path);
|
||||
entry->setStale(true);
|
||||
m_filesNetJob.reset(new NetJob(tr("Modpack download")));
|
||||
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
|
||||
m_archivePath = entry->getFullPath();
|
||||
auto job = m_filesNetJob.get();
|
||||
connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
|
||||
connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
|
||||
connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
|
||||
m_filesNetJob->start();
|
||||
}
|
||||
|
||||
void Technic::SingleZipPackInstallTask::downloadSucceeded()
|
||||
{
|
||||
setStatus(tr("Extracting modpack"));
|
||||
QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft"));
|
||||
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;
|
||||
}
|
||||
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted);
|
||||
m_extractFutureWatcher.setFuture(m_extractFuture);
|
||||
m_filesNetJob.reset();
|
||||
}
|
||||
|
||||
void Technic::SingleZipPackInstallTask::downloadFailed(QString reason)
|
||||
{
|
||||
emitFailed(reason);
|
||||
m_filesNetJob.reset();
|
||||
}
|
||||
|
||||
void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
|
||||
{
|
||||
setProgress(current / 2, total);
|
||||
}
|
||||
|
||||
void Technic::SingleZipPackInstallTask::extractFinished()
|
||||
{
|
||||
m_packZip.reset();
|
||||
if (m_extractFuture.result().isEmpty())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
|
||||
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
|
||||
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
|
||||
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion);
|
||||
}
|
||||
|
||||
void Technic::SingleZipPackInstallTask::extractAborted()
|
||||
{
|
||||
emitFailed(tr("Instance import has been aborted."));
|
||||
}
|
||||
58
api/logic/modplatform/technic/SingleZipPackInstallTask.h
Normal file
58
api/logic/modplatform/technic/SingleZipPackInstallTask.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/* Copyright 2013-2020 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 "InstanceTask.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
#include "quazip.h"
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
namespace Technic {
|
||||
|
||||
class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
|
||||
|
||||
protected:
|
||||
void executeTask() override;
|
||||
|
||||
|
||||
private slots:
|
||||
void downloadSucceeded();
|
||||
void downloadFailed(QString reason);
|
||||
void downloadProgressChanged(qint64 current, qint64 total);
|
||||
void extractFinished();
|
||||
void extractAborted();
|
||||
|
||||
private:
|
||||
QUrl m_sourceUrl;
|
||||
QString m_minecraftVersion;
|
||||
QString m_archivePath;
|
||||
NetJobPtr m_filesNetJob;
|
||||
std::unique_ptr<QuaZip> m_packZip;
|
||||
QFuture<QStringList> m_extractFuture;
|
||||
QFutureWatcher<QStringList> m_extractFutureWatcher;
|
||||
};
|
||||
|
||||
} // namespace Technic
|
||||
195
api/logic/modplatform/technic/SolderPackInstallTask.cpp
Normal file
195
api/logic/modplatform/technic/SolderPackInstallTask.cpp
Normal file
@@ -0,0 +1,195 @@
|
||||
/* Copyright 2013-2020 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 "SolderPackInstallTask.h"
|
||||
|
||||
#include <FileSystem.h>
|
||||
#include <Json.h>
|
||||
#include <QtConcurrentRun>
|
||||
#include <MMCZip.h>
|
||||
#include "TechnicPackProcessor.h"
|
||||
|
||||
Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
|
||||
{
|
||||
m_sourceUrl = sourceUrl;
|
||||
m_minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
|
||||
m_filesNetJob.reset(new NetJob(tr("Finding recommended version")));
|
||||
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
|
||||
auto job = m_filesNetJob.get();
|
||||
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded);
|
||||
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
|
||||
m_filesNetJob->start();
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::versionSucceeded()
|
||||
{
|
||||
try
|
||||
{
|
||||
QJsonDocument doc = Json::requireDocument(m_response);
|
||||
QJsonObject obj = Json::requireObject(doc);
|
||||
QString version = Json::requireString(obj, "recommended", "__placeholder__");
|
||||
m_sourceUrl = m_sourceUrl.toString() + '/' + version;
|
||||
}
|
||||
catch (const JSONValidationError &e)
|
||||
{
|
||||
emitFailed(e.cause());
|
||||
m_filesNetJob.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
|
||||
m_filesNetJob.reset(new NetJob(tr("Resolving modpack files")));
|
||||
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
|
||||
auto job = m_filesNetJob.get();
|
||||
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
|
||||
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
|
||||
m_filesNetJob->start();
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::fileListSucceeded()
|
||||
{
|
||||
setStatus(tr("Downloading modpack:"));
|
||||
QStringList modUrls;
|
||||
try
|
||||
{
|
||||
QJsonDocument doc = Json::requireDocument(m_response);
|
||||
QJsonObject obj = Json::requireObject(doc);
|
||||
QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
|
||||
if (!minecraftVersion.isEmpty())
|
||||
m_minecraftVersion = minecraftVersion;
|
||||
QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
|
||||
for (auto mod: mods)
|
||||
{
|
||||
QJsonObject modObject = Json::requireObject(mod);
|
||||
modUrls.append(Json::requireString(modObject, "url", "'url'"));
|
||||
}
|
||||
}
|
||||
catch (const JSONValidationError &e)
|
||||
{
|
||||
emitFailed(e.cause());
|
||||
m_filesNetJob.reset();
|
||||
return;
|
||||
}
|
||||
m_filesNetJob.reset(new NetJob(tr("Downloading modpack")));
|
||||
int i = 0;
|
||||
for (auto &modUrl: modUrls)
|
||||
{
|
||||
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
|
||||
m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path));
|
||||
i++;
|
||||
}
|
||||
|
||||
m_modCount = modUrls.size();
|
||||
|
||||
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
|
||||
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
|
||||
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
|
||||
m_filesNetJob->start();
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::downloadSucceeded()
|
||||
{
|
||||
setStatus(tr("Extracting modpack"));
|
||||
m_filesNetJob.reset();
|
||||
m_extractFuture = QtConcurrent::run([this]()
|
||||
{
|
||||
int i = 0;
|
||||
QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft");
|
||||
FS::ensureFolderPathExists(extractDir);
|
||||
|
||||
while (m_modCount > i)
|
||||
{
|
||||
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
|
||||
if (MMCZip::extractDir(path, extractDir).isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SolderPackInstallTask::extractFinished);
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SolderPackInstallTask::extractAborted);
|
||||
m_extractFutureWatcher.setFuture(m_extractFuture);
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::downloadFailed(QString reason)
|
||||
{
|
||||
emitFailed(reason);
|
||||
m_filesNetJob.reset();
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
|
||||
{
|
||||
setProgress(current / 2, total);
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::extractFinished()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
|
||||
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
|
||||
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
|
||||
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true);
|
||||
}
|
||||
|
||||
void Technic::SolderPackInstallTask::extractAborted()
|
||||
{
|
||||
emitFailed(tr("Instance import has been aborted."));
|
||||
return;
|
||||
}
|
||||
|
||||
55
api/logic/modplatform/technic/SolderPackInstallTask.h
Normal file
55
api/logic/modplatform/technic/SolderPackInstallTask.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/* Copyright 2013-2020 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 <InstanceTask.h>
|
||||
#include <net/NetJob.h>
|
||||
#include <tasks/Task.h>
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
namespace Technic
|
||||
{
|
||||
class MULTIMC_LOGIC_EXPORT SolderPackInstallTask : public InstanceTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
|
||||
|
||||
protected:
|
||||
//! Entry point for tasks.
|
||||
virtual void executeTask() override;
|
||||
|
||||
private slots:
|
||||
void versionSucceeded();
|
||||
void fileListSucceeded();
|
||||
void downloadSucceeded();
|
||||
void downloadFailed(QString reason);
|
||||
void downloadProgressChanged(qint64 current, qint64 total);
|
||||
void extractFinished();
|
||||
void extractAborted();
|
||||
|
||||
private:
|
||||
NetJobPtr m_filesNetJob;
|
||||
QUrl m_sourceUrl;
|
||||
QString m_minecraftVersion;
|
||||
QByteArray m_response;
|
||||
QTemporaryDir m_outputDir;
|
||||
int m_modCount;
|
||||
QFuture<bool> m_extractFuture;
|
||||
QFutureWatcher<bool> m_extractFutureWatcher;
|
||||
};
|
||||
}
|
||||
199
api/logic/modplatform/technic/TechnicPackProcessor.cpp
Normal file
199
api/logic/modplatform/technic/TechnicPackProcessor.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
/* Copyright 2020 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 "TechnicPackProcessor.h"
|
||||
|
||||
#include <FileSystem.h>
|
||||
#include <Json.h>
|
||||
#include <minecraft/MinecraftInstance.h>
|
||||
#include <minecraft/PackProfile.h>
|
||||
#include <quazip.h>
|
||||
#include <quazipdir.h>
|
||||
#include <quazipfile.h>
|
||||
#include <settings/INISettingsObject.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder)
|
||||
{
|
||||
QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft");
|
||||
QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
|
||||
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
|
||||
instanceSettings->registerSetting("InstanceType", "Legacy");
|
||||
instanceSettings->set("InstanceType", "OneSix");
|
||||
MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);
|
||||
|
||||
instance.setName(instName);
|
||||
|
||||
if (instIcon != "default")
|
||||
{
|
||||
instance.setIconKey(instIcon);
|
||||
}
|
||||
|
||||
auto components = instance.getPackProfile();
|
||||
components->buildingFromScratch();
|
||||
|
||||
QByteArray data;
|
||||
|
||||
QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar");
|
||||
QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
|
||||
QString fmlMinecraftVersion;
|
||||
if (QFile::exists(modpackJar))
|
||||
{
|
||||
QuaZip zipFile(modpackJar);
|
||||
if (!zipFile.open(QuaZip::mdUnzip))
|
||||
{
|
||||
emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
|
||||
return;
|
||||
}
|
||||
QuaZipDir zipFileRoot(&zipFile, "/");
|
||||
if (zipFileRoot.exists("/version.json"))
|
||||
{
|
||||
if (zipFileRoot.exists("/fmlversion.properties"))
|
||||
{
|
||||
zipFile.setCurrentFile("fmlversion.properties");
|
||||
QuaZipFile file(&zipFile);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
emit failed(tr("Unable to open \"fmlversion.properties\"!"));
|
||||
return;
|
||||
}
|
||||
QByteArray fmlVersionData = file.readAll();
|
||||
file.close();
|
||||
INIFile iniFile;
|
||||
iniFile.loadFile(fmlVersionData);
|
||||
// If not present, this evaluates to a null string
|
||||
fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
|
||||
}
|
||||
zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
|
||||
QuaZipFile file(&zipFile);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
emit failed(tr("Unable to open \"version.json\"!"));
|
||||
return;
|
||||
}
|
||||
data = file.readAll();
|
||||
file.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (minecraftVersion.isEmpty())
|
||||
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown"));
|
||||
components->setComponentVersion("net.minecraft", minecraftVersion, true);
|
||||
components->installJarMods({modpackJar});
|
||||
|
||||
// Forge for 1.4.7 and for 1.5.2 require extra libraries.
|
||||
// Figure out the forge version and add it as a component
|
||||
// (the code still comes from the jar mod installed above)
|
||||
if (zipFileRoot.exists("/forgeversion.properties"))
|
||||
{
|
||||
zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
|
||||
QuaZipFile file(&zipFile);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
// Really shouldn't happen, but error handling shall not be forgotten
|
||||
emit failed(tr("Unable to open \"forgeversion.properties\""));
|
||||
return;
|
||||
}
|
||||
QByteArray forgeVersionData = file.readAll();
|
||||
file.close();
|
||||
INIFile iniFile;
|
||||
iniFile.loadFile(forgeVersionData);
|
||||
QString major, minor, revision, build;
|
||||
major = iniFile["forge.major.number"].toString();
|
||||
minor = iniFile["forge.minor.number"].toString();
|
||||
revision = iniFile["forge.revision.number"].toString();
|
||||
build = iniFile["forge.build.number"].toString();
|
||||
|
||||
if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty())
|
||||
{
|
||||
emit failed(tr("Invalid \"forgeversion.properties\"!"));
|
||||
return;
|
||||
}
|
||||
|
||||
components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build);
|
||||
}
|
||||
|
||||
components->saveNow();
|
||||
emit succeeded();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (QFile::exists(versionJson))
|
||||
{
|
||||
QFile file(versionJson);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
emit failed(tr("Unable to open \"version.json\"!"));
|
||||
return;
|
||||
}
|
||||
data = file.readAll();
|
||||
file.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the "Vanilla" modpack, excluded by the search code
|
||||
emit failed(tr("Unable to find a \"version.json\"!"));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
QJsonDocument doc = Json::requireDocument(data);
|
||||
QJsonObject root = Json::requireObject(doc, "version.json");
|
||||
QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
|
||||
if (minecraftVersion.isEmpty())
|
||||
{
|
||||
if (fmlMinecraftVersion.isEmpty())
|
||||
{
|
||||
emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
|
||||
return;
|
||||
}
|
||||
minecraftVersion = fmlMinecraftVersion;
|
||||
}
|
||||
components->setComponentVersion("net.minecraft", minecraftVersion, true);
|
||||
for (auto library: Json::ensureArray(root, "libraries", {}))
|
||||
{
|
||||
if (!library.isObject())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto libraryObject = Json::ensureObject(library, {}, "");
|
||||
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
|
||||
|
||||
if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-'))
|
||||
{
|
||||
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
|
||||
}
|
||||
else if (libraryName.startsWith("net.minecraftforge:minecraftforge:"))
|
||||
{
|
||||
components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2));
|
||||
}
|
||||
else if (libraryName.startsWith("net.fabricmc:fabric-loader:"))
|
||||
{
|
||||
components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const JSONValidationError &e)
|
||||
{
|
||||
emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
components->saveNow();
|
||||
emit succeeded();
|
||||
}
|
||||
35
api/logic/modplatform/technic/TechnicPackProcessor.h
Normal file
35
api/logic/modplatform/technic/TechnicPackProcessor.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/* Copyright 2020 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 <QString>
|
||||
#include "settings/SettingsObject.h"
|
||||
|
||||
namespace Technic
|
||||
{
|
||||
// not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask
|
||||
class TechnicPackProcessor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void succeeded();
|
||||
void failed(QString reason);
|
||||
|
||||
public:
|
||||
void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false);
|
||||
};
|
||||
}
|
||||
427
api/logic/mojang/PackageManifest.cpp
Normal file
427
api/logic/mojang/PackageManifest.cpp
Normal file
@@ -0,0 +1,427 @@
|
||||
#include "PackageManifest.h"
|
||||
#include <Json.h>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
namespace mojang_files {
|
||||
|
||||
const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
|
||||
|
||||
int Path::compare(const Path& rhs) const
|
||||
{
|
||||
auto left_cursor = begin();
|
||||
auto left_end = end();
|
||||
auto right_cursor = rhs.begin();
|
||||
auto right_end = rhs.end();
|
||||
|
||||
while (left_cursor != left_end && right_cursor != right_end)
|
||||
{
|
||||
if(*left_cursor < *right_cursor)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if(*left_cursor > *right_cursor)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
left_cursor++;
|
||||
right_cursor++;
|
||||
}
|
||||
|
||||
if(left_cursor == left_end)
|
||||
{
|
||||
if(right_cursor == right_end)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Package::addFile(const Path& path, const File& file) {
|
||||
addFolder(path.parent_path());
|
||||
files[path] = file;
|
||||
}
|
||||
|
||||
void Package::addFolder(Path folder) {
|
||||
if(!folder.has_parent_path()) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
folders.insert(folder);
|
||||
folder = folder.parent_path();
|
||||
} while(folder.has_parent_path());
|
||||
}
|
||||
|
||||
void Package::addLink(const Path& path, const Path& target) {
|
||||
addFolder(path.parent_path());
|
||||
symlinks[path] = target;
|
||||
}
|
||||
|
||||
void Package::addSource(const FileSource& source) {
|
||||
sources[source.hash] = source;
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
void fromJson(QJsonDocument & doc, Package & out) {
|
||||
std::set<Path> seen_paths;
|
||||
if (!doc.isObject())
|
||||
{
|
||||
throw JSONValidationError("file manifest is not an object");
|
||||
}
|
||||
QJsonObject root = doc.object();
|
||||
|
||||
auto filesObj = Json::ensureObject(root, "files");
|
||||
auto iter = filesObj.begin();
|
||||
while (iter != filesObj.end())
|
||||
{
|
||||
Path objectPath = Path(iter.key());
|
||||
auto value = iter.value();
|
||||
iter++;
|
||||
if(seen_paths.count(objectPath)) {
|
||||
throw JSONValidationError("duplicate path inside manifest, the manifest is invalid");
|
||||
}
|
||||
if (!value.isObject())
|
||||
{
|
||||
throw JSONValidationError("file entry inside manifest is not an an object");
|
||||
}
|
||||
seen_paths.insert(objectPath);
|
||||
|
||||
auto fileObject = value.toObject();
|
||||
auto type = Json::requireString(fileObject, "type");
|
||||
if(type == "directory") {
|
||||
out.addFolder(objectPath);
|
||||
continue;
|
||||
}
|
||||
else if(type == "file") {
|
||||
FileSource bestSource;
|
||||
File file;
|
||||
file.executable = Json::ensureBoolean(fileObject, QString("executable"), false);
|
||||
auto downloads = Json::requireObject(fileObject, "downloads");
|
||||
for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) {
|
||||
FileSource source;
|
||||
|
||||
auto downloadObject = Json::requireObject(iter2.value());
|
||||
source.hash = Json::requireString(downloadObject, "sha1");
|
||||
source.size = Json::requireInteger(downloadObject, "size");
|
||||
source.url = Json::requireString(downloadObject, "url");
|
||||
|
||||
auto compression = iter2.key();
|
||||
if(compression == "raw") {
|
||||
file.hash = source.hash;
|
||||
file.size = source.size;
|
||||
source.compression = Compression::Raw;
|
||||
}
|
||||
else if (compression == "lzma") {
|
||||
source.compression = Compression::Lzma;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
bestSource.upgrade(source);
|
||||
}
|
||||
if(bestSource.isBad()) {
|
||||
throw JSONValidationError("No valid compression method for file " + iter.key());
|
||||
}
|
||||
out.addFile(objectPath, file);
|
||||
out.addSource(bestSource);
|
||||
}
|
||||
else if(type == "link") {
|
||||
auto target = Json::requireString(fileObject, "target");
|
||||
out.symlinks[objectPath] = target;
|
||||
out.addLink(objectPath, target);
|
||||
}
|
||||
else {
|
||||
throw JSONValidationError("Invalid item type in manifest: " + type);
|
||||
}
|
||||
}
|
||||
// make sure the containing folder exists
|
||||
out.folders.insert(Path());
|
||||
}
|
||||
}
|
||||
|
||||
Package Package::fromManifestContents(const QByteArray& contents)
|
||||
{
|
||||
Package out;
|
||||
try
|
||||
{
|
||||
auto doc = Json::requireDocument(contents, "Manifest");
|
||||
fromJson(doc, out);
|
||||
return out;
|
||||
}
|
||||
catch (const Exception &e)
|
||||
{
|
||||
qDebug() << QString("Unable to parse manifest: %1").arg(e.cause());
|
||||
out.valid = false;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
Package Package::fromManifestFile(const QString & filename) {
|
||||
Package out;
|
||||
try
|
||||
{
|
||||
auto doc = Json::requireDocument(filename, filename);
|
||||
fromJson(doc, out);
|
||||
return out;
|
||||
}
|
||||
catch (const Exception &e)
|
||||
{
|
||||
qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause());
|
||||
out.valid = false;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
namespace {
|
||||
// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves
|
||||
bool actually_read_symlink_target(const QString & filepath, Path & out)
|
||||
{
|
||||
struct ::stat st;
|
||||
// FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls.
|
||||
QByteArray nativePath = filepath.toUtf8();
|
||||
const char * filepath_cstr = nativePath.data();
|
||||
|
||||
if (lstat(filepath_cstr, &st) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto size = st.st_size ? st.st_size + 1 : PATH_MAX;
|
||||
std::string temp(size, '\0');
|
||||
// because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff
|
||||
do
|
||||
{
|
||||
auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size());
|
||||
if(link_length == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(std::string::size_type(link_length) < temp.size())
|
||||
{
|
||||
// buffer was long enough and we managed to read the link target. RETURN here.
|
||||
temp.resize(link_length);
|
||||
out = Path(QString::fromUtf8(temp.c_str()));
|
||||
return true;
|
||||
}
|
||||
temp.resize(temp.size() * 2);
|
||||
} while (true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much?
|
||||
// FIXME: The error handling is just DEFICIENT
|
||||
Package Package::fromInspectedFolder(const QString& folderPath)
|
||||
{
|
||||
QDir root(folderPath);
|
||||
|
||||
Package out;
|
||||
QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
|
||||
while(iterator.hasNext()) {
|
||||
iterator.next();
|
||||
|
||||
auto fileInfo = iterator.fileInfo();
|
||||
auto relPath = root.relativeFilePath(fileInfo.filePath());
|
||||
// FIXME: this is probably completely busted on Windows anyway, so just disable it.
|
||||
// Qt makes shit up and doesn't understand the platform details
|
||||
// TODO: Actually use a filesystem library that isn't terrible and has decen license.
|
||||
// I only know one, and I wrote it. Sadly, currently proprietary. PAIN.
|
||||
#ifndef Q_OS_WIN32
|
||||
if(fileInfo.isSymLink()) {
|
||||
Path targetPath;
|
||||
if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) {
|
||||
qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
|
||||
out.valid = false;
|
||||
}
|
||||
out.addLink(relPath, targetPath);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if(fileInfo.isDir()) {
|
||||
out.addFolder(relPath);
|
||||
}
|
||||
else if(fileInfo.isFile()) {
|
||||
File f;
|
||||
f.executable = fileInfo.isExecutable();
|
||||
f.size = fileInfo.size();
|
||||
// FIXME: async / optimize the hashing
|
||||
QFile input(fileInfo.absoluteFilePath());
|
||||
if(!input.open(QIODevice::ReadOnly)) {
|
||||
qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath();
|
||||
out.valid = false;
|
||||
break;
|
||||
}
|
||||
f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData();
|
||||
out.addFile(relPath, f);
|
||||
}
|
||||
else {
|
||||
// Something else... oh my
|
||||
qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
|
||||
out.valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.folders.insert(Path("."));
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct shallow_first_sort
|
||||
{
|
||||
bool operator()(const Path &lhs, const Path &rhs) const
|
||||
{
|
||||
auto lhs_depth = lhs.length();
|
||||
auto rhs_depth = rhs.length();
|
||||
if(lhs_depth < rhs_depth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(lhs_depth == rhs_depth)
|
||||
{
|
||||
if(lhs < rhs)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct deep_first_sort
|
||||
{
|
||||
bool operator()(const Path &lhs, const Path &rhs) const
|
||||
{
|
||||
auto lhs_depth = lhs.length();
|
||||
auto rhs_depth = rhs.length();
|
||||
if(lhs_depth > rhs_depth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(lhs_depth == rhs_depth)
|
||||
{
|
||||
if(lhs < rhs)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to)
|
||||
{
|
||||
UpdateOperations out;
|
||||
|
||||
if(!from.valid || !to.valid) {
|
||||
out.valid = false;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Files
|
||||
for(auto iter = from.files.begin(); iter != from.files.end(); iter++) {
|
||||
const auto ¤t_hash = iter->second.hash;
|
||||
const auto ¤t_executable = iter->second.executable;
|
||||
const auto &path = iter->first;
|
||||
|
||||
auto iter2 = to.files.find(path);
|
||||
if(iter2 == to.files.end()) {
|
||||
// removed
|
||||
out.deletes.push_back(path);
|
||||
continue;
|
||||
}
|
||||
auto new_hash = iter2->second.hash;
|
||||
auto new_executable = iter2->second.executable;
|
||||
if (current_hash != new_hash) {
|
||||
out.deletes.push_back(path);
|
||||
out.downloads.emplace(
|
||||
std::pair<Path, FileDownload>{
|
||||
path,
|
||||
FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable)
|
||||
}
|
||||
);
|
||||
}
|
||||
else if (current_executable != new_executable) {
|
||||
out.executable_fixes[path] = new_executable;
|
||||
}
|
||||
}
|
||||
for(auto iter = to.files.begin(); iter != to.files.end(); iter++) {
|
||||
auto path = iter->first;
|
||||
if(!from.files.count(path)) {
|
||||
out.downloads.emplace(
|
||||
std::pair<Path, FileDownload>{
|
||||
path,
|
||||
FileDownload(to.sources.at(iter->second.hash), iter->second.executable)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Folders
|
||||
std::set<Path, deep_first_sort> remove_folders;
|
||||
std::set<Path, shallow_first_sort> make_folders;
|
||||
for(auto from_path: from.folders) {
|
||||
auto iter = to.folders.find(from_path);
|
||||
if(iter == to.folders.end()) {
|
||||
remove_folders.insert(from_path);
|
||||
}
|
||||
}
|
||||
for(auto & rmdir: remove_folders) {
|
||||
out.rmdirs.push_back(rmdir);
|
||||
}
|
||||
for(auto to_path: to.folders) {
|
||||
auto iter = from.folders.find(to_path);
|
||||
if(iter == from.folders.end()) {
|
||||
make_folders.insert(to_path);
|
||||
}
|
||||
}
|
||||
for(auto & mkdir: make_folders) {
|
||||
out.mkdirs.push_back(mkdir);
|
||||
}
|
||||
|
||||
// Symlinks
|
||||
for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) {
|
||||
const auto ¤t_target = iter->second;
|
||||
const auto &path = iter->first;
|
||||
|
||||
auto iter2 = to.symlinks.find(path);
|
||||
if(iter2 == to.symlinks.end()) {
|
||||
// removed
|
||||
out.deletes.push_back(path);
|
||||
continue;
|
||||
}
|
||||
const auto &new_target = iter2->second;
|
||||
if (current_target != new_target) {
|
||||
out.deletes.push_back(path);
|
||||
out.mklinks[path] = iter2->second;
|
||||
}
|
||||
}
|
||||
for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) {
|
||||
auto path = iter->first;
|
||||
if(!from.symlinks.count(path)) {
|
||||
out.mklinks[path] = iter->second;
|
||||
}
|
||||
}
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
173
api/logic/mojang/PackageManifest.h
Normal file
173
api/logic/mojang/PackageManifest.h
Normal file
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <QStringList>
|
||||
#include "tasks/Task.h"
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
namespace mojang_files {
|
||||
|
||||
using Hash = QString;
|
||||
extern const Hash empty_hash;
|
||||
|
||||
// simple-ish path implementation. assumes always relative and does not allow '..' entries
|
||||
class MULTIMC_LOGIC_EXPORT Path
|
||||
{
|
||||
public:
|
||||
using parts_type = QStringList;
|
||||
|
||||
Path() = default;
|
||||
Path(QString string) {
|
||||
auto parts_in = string.split('/');
|
||||
for(auto & part: parts_in) {
|
||||
if(part.isEmpty() || part == ".") {
|
||||
continue;
|
||||
}
|
||||
if(part == "..") {
|
||||
if(parts.size()) {
|
||||
parts.pop_back();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
parts.push_back(part);
|
||||
}
|
||||
}
|
||||
|
||||
bool has_parent_path() const
|
||||
{
|
||||
return parts.size() > 0;
|
||||
}
|
||||
|
||||
Path parent_path() const
|
||||
{
|
||||
if (parts.empty())
|
||||
return Path();
|
||||
return Path(parts.begin(), std::prev(parts.end()));
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return parts.empty();
|
||||
}
|
||||
|
||||
int length() const
|
||||
{
|
||||
return parts.length();
|
||||
}
|
||||
|
||||
bool operator==(const Path & rhs) const {
|
||||
return parts == rhs.parts;
|
||||
}
|
||||
|
||||
bool operator!=(const Path & rhs) const {
|
||||
return parts != rhs.parts;
|
||||
}
|
||||
|
||||
inline bool operator<(const Path& rhs) const
|
||||
{
|
||||
return compare(rhs) < 0;
|
||||
}
|
||||
|
||||
parts_type::const_iterator begin() const
|
||||
{
|
||||
return parts.begin();
|
||||
}
|
||||
|
||||
parts_type::const_iterator end() const
|
||||
{
|
||||
return parts.end();
|
||||
}
|
||||
|
||||
QString toString() const {
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
private:
|
||||
Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) {
|
||||
auto cursor = start;
|
||||
while(cursor != end) {
|
||||
parts.push_back(*cursor);
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
int compare(const Path& p) const;
|
||||
|
||||
parts_type parts;
|
||||
};
|
||||
|
||||
|
||||
enum class Compression {
|
||||
Raw,
|
||||
Lzma,
|
||||
Unknown
|
||||
};
|
||||
|
||||
|
||||
struct MULTIMC_LOGIC_EXPORT FileSource
|
||||
{
|
||||
Compression compression = Compression::Unknown;
|
||||
Hash hash;
|
||||
QString url;
|
||||
std::size_t size = 0;
|
||||
void upgrade(const FileSource & other) {
|
||||
if(compression == Compression::Unknown || other.size < size) {
|
||||
*this = other;
|
||||
}
|
||||
}
|
||||
bool isBad() const {
|
||||
return compression == Compression::Unknown;
|
||||
}
|
||||
};
|
||||
|
||||
struct MULTIMC_LOGIC_EXPORT File
|
||||
{
|
||||
Hash hash;
|
||||
bool executable;
|
||||
std::uint64_t size = 0;
|
||||
};
|
||||
|
||||
struct MULTIMC_LOGIC_EXPORT Package {
|
||||
static Package fromInspectedFolder(const QString &folderPath);
|
||||
static Package fromManifestFile(const QString &path);
|
||||
static Package fromManifestContents(const QByteArray& contents);
|
||||
|
||||
explicit operator bool() const
|
||||
{
|
||||
return valid;
|
||||
}
|
||||
void addFolder(Path folder);
|
||||
void addFile(const Path & path, const File & file);
|
||||
void addLink(const Path & path, const Path & target);
|
||||
void addSource(const FileSource & source);
|
||||
|
||||
std::map<Hash, FileSource> sources;
|
||||
bool valid = true;
|
||||
std::set<Path> folders;
|
||||
std::map<Path, File> files;
|
||||
std::map<Path, Path> symlinks;
|
||||
};
|
||||
|
||||
struct MULTIMC_LOGIC_EXPORT FileDownload : FileSource
|
||||
{
|
||||
FileDownload(const FileSource& source, bool executable) {
|
||||
static_cast<FileSource &> (*this) = source;
|
||||
this->executable = executable;
|
||||
}
|
||||
bool executable = false;
|
||||
};
|
||||
|
||||
struct MULTIMC_LOGIC_EXPORT UpdateOperations {
|
||||
static UpdateOperations resolve(const Package & from, const Package & to);
|
||||
bool valid = false;
|
||||
std::vector<Path> deletes;
|
||||
std::vector<Path> rmdirs;
|
||||
std::vector<Path> mkdirs;
|
||||
std::map<Path, FileDownload> downloads;
|
||||
std::map<Path, Path> mklinks;
|
||||
std::map<Path, bool> executable_fixes;
|
||||
};
|
||||
|
||||
}
|
||||
344
api/logic/mojang/PackageManifest_test.cpp
Normal file
344
api/logic/mojang/PackageManifest_test.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include <QTest>
|
||||
#include <QDebug>
|
||||
#include "TestUtil.h"
|
||||
|
||||
#include "mojang/PackageManifest.h"
|
||||
|
||||
using namespace mojang_files;
|
||||
|
||||
QDebug operator<<(QDebug debug, const Path &path)
|
||||
{
|
||||
debug << path.toString();
|
||||
return debug;
|
||||
}
|
||||
|
||||
class PackageManifestTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void test_parse();
|
||||
void test_parse_file();
|
||||
void test_inspect();
|
||||
#ifndef Q_OS_WIN32
|
||||
void test_inspect_symlinks();
|
||||
#endif
|
||||
void mkdir_deep();
|
||||
void rmdir_deep();
|
||||
|
||||
void identical_file();
|
||||
void changed_file();
|
||||
void added_file();
|
||||
void removed_file();
|
||||
};
|
||||
|
||||
namespace {
|
||||
QByteArray basic_manifest = R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b.txt": {
|
||||
"type": "file",
|
||||
"downloads": {
|
||||
"raw": {
|
||||
"url": "http://dethware.org/b.txt",
|
||||
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
"size": 0
|
||||
}
|
||||
},
|
||||
"executable": true
|
||||
},
|
||||
"a/b/c": {
|
||||
"type": "directory"
|
||||
},
|
||||
"a/b/c.txt": {
|
||||
"type": "link",
|
||||
"target": "../b.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
)END";
|
||||
}
|
||||
|
||||
void PackageManifestTest::test_parse()
|
||||
{
|
||||
auto manifest = Package::fromManifestContents(basic_manifest);
|
||||
QVERIFY(manifest.valid == true);
|
||||
QVERIFY(manifest.files.size() == 1);
|
||||
QVERIFY(manifest.files.count(Path("a/b.txt")));
|
||||
auto &file = manifest.files[Path("a/b.txt")];
|
||||
QVERIFY(file.executable == true);
|
||||
QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
|
||||
QVERIFY(file.size == 0);
|
||||
QVERIFY(manifest.folders.size() == 4);
|
||||
QVERIFY(manifest.folders.count(Path(".")));
|
||||
QVERIFY(manifest.folders.count(Path("a")));
|
||||
QVERIFY(manifest.folders.count(Path("a/b")));
|
||||
QVERIFY(manifest.folders.count(Path("a/b/c")));
|
||||
QVERIFY(manifest.symlinks.size() == 1);
|
||||
auto symlinkPath = Path("a/b/c.txt");
|
||||
QVERIFY(manifest.symlinks.count(symlinkPath));
|
||||
auto &symlink = manifest.symlinks[symlinkPath];
|
||||
QVERIFY(symlink == Path("../b.txt"));
|
||||
QVERIFY(manifest.sources.size() == 1);
|
||||
}
|
||||
|
||||
void PackageManifestTest::test_parse_file() {
|
||||
auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json");
|
||||
auto manifest = Package::fromManifestFile(path);
|
||||
QVERIFY(manifest.valid == true);
|
||||
}
|
||||
|
||||
|
||||
void PackageManifestTest::test_inspect() {
|
||||
auto path = QFINDTESTDATA("testdata/inspect_win/");
|
||||
auto manifest = Package::fromInspectedFolder(path);
|
||||
QVERIFY(manifest.valid == true);
|
||||
QVERIFY(manifest.files.size() == 2);
|
||||
QVERIFY(manifest.files.count(Path("a/b.txt")));
|
||||
auto &file1 = manifest.files[Path("a/b.txt")];
|
||||
QVERIFY(file1.executable == false);
|
||||
QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
|
||||
QVERIFY(file1.size == 0);
|
||||
QVERIFY(manifest.files.count(Path("a/b/b.txt")));
|
||||
auto &file2 = manifest.files[Path("a/b/b.txt")];
|
||||
QVERIFY(file2.executable == false);
|
||||
QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
|
||||
QVERIFY(file2.size == 0);
|
||||
QVERIFY(manifest.folders.size() == 3);
|
||||
QVERIFY(manifest.folders.count(Path(".")));
|
||||
QVERIFY(manifest.folders.count(Path("a")));
|
||||
QVERIFY(manifest.folders.count(Path("a/b")));
|
||||
QVERIFY(manifest.symlinks.size() == 0);
|
||||
}
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
void PackageManifestTest::test_inspect_symlinks() {
|
||||
auto path = QFINDTESTDATA("testdata/inspect/");
|
||||
auto manifest = Package::fromInspectedFolder(path);
|
||||
QVERIFY(manifest.valid == true);
|
||||
QVERIFY(manifest.files.size() == 1);
|
||||
QVERIFY(manifest.files.count(Path("a/b.txt")));
|
||||
auto &file = manifest.files[Path("a/b.txt")];
|
||||
QVERIFY(file.executable == true);
|
||||
QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
|
||||
QVERIFY(file.size == 0);
|
||||
QVERIFY(manifest.folders.size() == 3);
|
||||
QVERIFY(manifest.folders.count(Path(".")));
|
||||
QVERIFY(manifest.folders.count(Path("a")));
|
||||
QVERIFY(manifest.folders.count(Path("a/b")));
|
||||
QVERIFY(manifest.symlinks.size() == 1);
|
||||
QVERIFY(manifest.symlinks.count(Path("a/b/b.txt")));
|
||||
qDebug() << manifest.symlinks[Path("a/b/b.txt")];
|
||||
QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt"));
|
||||
}
|
||||
#endif
|
||||
|
||||
void PackageManifestTest::mkdir_deep() {
|
||||
|
||||
Package from;
|
||||
auto to = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/e": {
|
||||
"type": "directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto operations = UpdateOperations::resolve(from, to);
|
||||
QVERIFY(operations.deletes.size() == 0);
|
||||
QVERIFY(operations.rmdirs.size() == 0);
|
||||
|
||||
QVERIFY(operations.mkdirs.size() == 6);
|
||||
QVERIFY(operations.mkdirs[0] == Path("."));
|
||||
QVERIFY(operations.mkdirs[1] == Path("a"));
|
||||
QVERIFY(operations.mkdirs[2] == Path("a/b"));
|
||||
QVERIFY(operations.mkdirs[3] == Path("a/b/c"));
|
||||
QVERIFY(operations.mkdirs[4] == Path("a/b/c/d"));
|
||||
QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e"));
|
||||
|
||||
QVERIFY(operations.downloads.size() == 0);
|
||||
QVERIFY(operations.mklinks.size() == 0);
|
||||
QVERIFY(operations.executable_fixes.size() == 0);
|
||||
}
|
||||
|
||||
void PackageManifestTest::rmdir_deep() {
|
||||
|
||||
Package to;
|
||||
auto from = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/e": {
|
||||
"type": "directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto operations = UpdateOperations::resolve(from, to);
|
||||
QVERIFY(operations.deletes.size() == 0);
|
||||
|
||||
QVERIFY(operations.rmdirs.size() == 6);
|
||||
QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e"));
|
||||
QVERIFY(operations.rmdirs[1] == Path("a/b/c/d"));
|
||||
QVERIFY(operations.rmdirs[2] == Path("a/b/c"));
|
||||
QVERIFY(operations.rmdirs[3] == Path("a/b"));
|
||||
QVERIFY(operations.rmdirs[4] == Path("a"));
|
||||
QVERIFY(operations.rmdirs[5] == Path("."));
|
||||
|
||||
QVERIFY(operations.mkdirs.size() == 0);
|
||||
QVERIFY(operations.downloads.size() == 0);
|
||||
QVERIFY(operations.mklinks.size() == 0);
|
||||
QVERIFY(operations.executable_fixes.size() == 0);
|
||||
}
|
||||
|
||||
void PackageManifestTest::identical_file() {
|
||||
QByteArray manifest = R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/empty.txt": {
|
||||
"type": "file",
|
||||
"downloads": {
|
||||
"raw": {
|
||||
"url": "http://dethware.org/empty.txt",
|
||||
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
"size": 0
|
||||
}
|
||||
},
|
||||
"executable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
)END";
|
||||
auto from = Package::fromManifestContents(manifest);
|
||||
auto to = Package::fromManifestContents(manifest);
|
||||
auto operations = UpdateOperations::resolve(from, to);
|
||||
QVERIFY(operations.deletes.size() == 0);
|
||||
QVERIFY(operations.rmdirs.size() == 0);
|
||||
QVERIFY(operations.mkdirs.size() == 0);
|
||||
QVERIFY(operations.downloads.size() == 0);
|
||||
QVERIFY(operations.mklinks.size() == 0);
|
||||
QVERIFY(operations.executable_fixes.size() == 0);
|
||||
}
|
||||
|
||||
void PackageManifestTest::changed_file() {
|
||||
auto from = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/file": {
|
||||
"type": "file",
|
||||
"downloads": {
|
||||
"raw": {
|
||||
"url": "http://dethware.org/empty.txt",
|
||||
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
"size": 0
|
||||
}
|
||||
},
|
||||
"executable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto to = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/file": {
|
||||
"type": "file",
|
||||
"downloads": {
|
||||
"raw": {
|
||||
"url": "http://dethware.org/space.txt",
|
||||
"sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
|
||||
"size": 1
|
||||
}
|
||||
},
|
||||
"executable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto operations = UpdateOperations::resolve(from, to);
|
||||
QVERIFY(operations.deletes.size() == 1);
|
||||
QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
|
||||
QVERIFY(operations.rmdirs.size() == 0);
|
||||
QVERIFY(operations.mkdirs.size() == 0);
|
||||
QVERIFY(operations.downloads.size() == 1);
|
||||
QVERIFY(operations.mklinks.size() == 0);
|
||||
QVERIFY(operations.executable_fixes.size() == 0);
|
||||
}
|
||||
|
||||
void PackageManifestTest::added_file() {
|
||||
auto from = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d": {
|
||||
"type": "directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto to = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/file": {
|
||||
"type": "file",
|
||||
"downloads": {
|
||||
"raw": {
|
||||
"url": "http://dethware.org/space.txt",
|
||||
"sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
|
||||
"size": 1
|
||||
}
|
||||
},
|
||||
"executable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto operations = UpdateOperations::resolve(from, to);
|
||||
QVERIFY(operations.deletes.size() == 0);
|
||||
QVERIFY(operations.rmdirs.size() == 0);
|
||||
QVERIFY(operations.mkdirs.size() == 0);
|
||||
QVERIFY(operations.downloads.size() == 1);
|
||||
QVERIFY(operations.mklinks.size() == 0);
|
||||
QVERIFY(operations.executable_fixes.size() == 0);
|
||||
}
|
||||
|
||||
void PackageManifestTest::removed_file() {
|
||||
auto from = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d/file": {
|
||||
"type": "file",
|
||||
"downloads": {
|
||||
"raw": {
|
||||
"url": "http://dethware.org/space.txt",
|
||||
"sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
|
||||
"size": 1
|
||||
}
|
||||
},
|
||||
"executable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto to = Package::fromManifestContents(R"END(
|
||||
{
|
||||
"files": {
|
||||
"a/b/c/d": {
|
||||
"type": "directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
)END");
|
||||
auto operations = UpdateOperations::resolve(from, to);
|
||||
QVERIFY(operations.deletes.size() == 1);
|
||||
QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
|
||||
QVERIFY(operations.rmdirs.size() == 0);
|
||||
QVERIFY(operations.mkdirs.size() == 0);
|
||||
QVERIFY(operations.downloads.size() == 0);
|
||||
QVERIFY(operations.mklinks.size() == 0);
|
||||
QVERIFY(operations.executable_fixes.size() == 0);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(PackageManifestTest)
|
||||
|
||||
#include "PackageManifest_test.moc"
|
||||
|
||||
1
api/logic/mojang/testdata/1.8.0_202-x64.json
vendored
Normal file
1
api/logic/mojang/testdata/1.8.0_202-x64.json
vendored
Normal file
File diff suppressed because one or more lines are too long
0
api/logic/mojang/testdata/inspect/a/b.txt
vendored
Executable file
0
api/logic/mojang/testdata/inspect/a/b.txt
vendored
Executable file
1
api/logic/mojang/testdata/inspect/a/b/b.txt
vendored
Symbolic link
1
api/logic/mojang/testdata/inspect/a/b/b.txt
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../b.txt
|
||||
0
api/logic/mojang/testdata/inspect_win/a/b.txt
vendored
Normal file
0
api/logic/mojang/testdata/inspect_win/a/b.txt
vendored
Normal file
0
api/logic/mojang/testdata/inspect_win/a/b/b.txt
vendored
Normal file
0
api/logic/mojang/testdata/inspect_win/a/b/b.txt
vendored
Normal file
@@ -214,3 +214,5 @@ bool NetJob::addNetAction(NetActionPtr action)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
NetJob::~NetJob() = default;
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
{
|
||||
setObjectName(job_name);
|
||||
}
|
||||
virtual ~NetJob() {}
|
||||
virtual ~NetJob();
|
||||
|
||||
bool addNetAction(NetActionPtr action);
|
||||
|
||||
|
||||
@@ -137,6 +137,10 @@ SET(MULTIMC_SOURCES
|
||||
pages/modplatform/twitch/TwitchModel.h
|
||||
pages/modplatform/twitch/TwitchPage.cpp
|
||||
pages/modplatform/twitch/TwitchPage.h
|
||||
pages/modplatform/technic/TechnicModel.cpp
|
||||
pages/modplatform/technic/TechnicModel.h
|
||||
pages/modplatform/technic/TechnicPage.cpp
|
||||
pages/modplatform/technic/TechnicPage.h
|
||||
pages/modplatform/ImportPage.cpp
|
||||
pages/modplatform/ImportPage.h
|
||||
|
||||
@@ -257,6 +261,7 @@ SET(MULTIMC_UIS
|
||||
pages/modplatform/ftb/FtbPage.ui
|
||||
pages/modplatform/legacy_ftb/Page.ui
|
||||
pages/modplatform/twitch/TwitchPage.ui
|
||||
pages/modplatform/technic/TechnicPage.ui
|
||||
pages/modplatform/ImportPage.ui
|
||||
|
||||
# Dialogs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright 2013-2019 MultiMC Contributors
|
||||
/* Copyright 2013-2020 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,6 +38,8 @@
|
||||
#include <pages/modplatform/legacy_ftb/Page.h>
|
||||
#include <pages/modplatform/twitch/TwitchPage.h>
|
||||
#include <pages/modplatform/ImportPage.h>
|
||||
#include <pages/modplatform/technic/TechnicPage.h>
|
||||
|
||||
|
||||
|
||||
NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent)
|
||||
@@ -122,12 +124,14 @@ QList<BasePage *> NewInstanceDialog::getPages()
|
||||
{
|
||||
importPage = new ImportPage(this);
|
||||
twitchPage = new TwitchPage(this);
|
||||
auto technicPage = new TechnicPage(this);
|
||||
return
|
||||
{
|
||||
new VanillaPage(this),
|
||||
importPage,
|
||||
new FtbPage(this),
|
||||
new LegacyFTB::Page(this),
|
||||
technicPage,
|
||||
twitchPage
|
||||
};
|
||||
}
|
||||
|
||||
39
application/pages/modplatform/technic/TechnicData.h
Normal file
39
application/pages/modplatform/technic/TechnicData.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/* Copyright 2020 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 <QString>
|
||||
|
||||
namespace Technic {
|
||||
struct Modpack {
|
||||
QString slug;
|
||||
|
||||
QString name;
|
||||
QString logoUrl;
|
||||
QString logoName;
|
||||
|
||||
bool broken = true;
|
||||
|
||||
QString url;
|
||||
bool isSolder = false;
|
||||
QString minecraftVersion;
|
||||
|
||||
bool metadataLoaded = false;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(Technic::Modpack)
|
||||
231
application/pages/modplatform/technic/TechnicModel.cpp
Normal file
231
application/pages/modplatform/technic/TechnicModel.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
/* Copyright 2020 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 "TechnicModel.h"
|
||||
#include "Env.h"
|
||||
#include "MultiMC.h"
|
||||
#include "Json.h"
|
||||
|
||||
#include <QIcon>
|
||||
|
||||
Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
Technic::ListModel::~ListModel()
|
||||
{
|
||||
}
|
||||
|
||||
QVariant Technic::ListModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
int pos = index.row();
|
||||
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
|
||||
{
|
||||
return QString("INVALID INDEX %1").arg(pos);
|
||||
}
|
||||
|
||||
Modpack pack = modpacks.at(pos);
|
||||
if(role == Qt::DisplayRole)
|
||||
{
|
||||
return pack.name;
|
||||
}
|
||||
else if(role == Qt::DecorationRole)
|
||||
{
|
||||
if(m_logoMap.contains(pack.logoName))
|
||||
{
|
||||
return (m_logoMap.value(pack.logoName));
|
||||
}
|
||||
QIcon icon = MMC->getThemedIcon("screenshot-placeholder");
|
||||
((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
|
||||
return icon;
|
||||
}
|
||||
else if(role == Qt::UserRole)
|
||||
{
|
||||
QVariant v;
|
||||
v.setValue(pack);
|
||||
return v;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
int Technic::ListModel::columnCount(const QModelIndex&) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Technic::ListModel::rowCount(const QModelIndex&) const
|
||||
{
|
||||
return modpacks.size();
|
||||
}
|
||||
|
||||
void Technic::ListModel::searchWithTerm(const QString& term)
|
||||
{
|
||||
if(currentSearchTerm == term) {
|
||||
return;
|
||||
}
|
||||
currentSearchTerm = term;
|
||||
if(jobPtr) {
|
||||
jobPtr->abort();
|
||||
searchState = ResetRequested;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
beginResetModel();
|
||||
modpacks.clear();
|
||||
endResetModel();
|
||||
searchState = None;
|
||||
}
|
||||
performSearch();
|
||||
}
|
||||
|
||||
void Technic::ListModel::performSearch()
|
||||
{
|
||||
NetJob *netJob = new NetJob("Technic::Search");
|
||||
auto searchUrl = QString(
|
||||
"https://api.technicpack.net/search?build=multimc&q=%1"
|
||||
).arg(currentSearchTerm);
|
||||
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
|
||||
jobPtr = netJob;
|
||||
jobPtr->start();
|
||||
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
|
||||
QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
|
||||
}
|
||||
|
||||
void Technic::ListModel::searchRequestFinished()
|
||||
{
|
||||
jobPtr.reset();
|
||||
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
|
||||
if(parse_error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
|
||||
qWarning() << response;
|
||||
return;
|
||||
}
|
||||
|
||||
QList<Modpack> newList;
|
||||
try {
|
||||
auto root = Json::requireObject(doc);
|
||||
auto objs = Json::requireArray(root, "modpacks");
|
||||
for (auto technicPack: objs) {
|
||||
Modpack pack;
|
||||
auto technicPackObject = Json::requireObject(technicPack);
|
||||
pack.name = Json::requireString(technicPackObject, "name");
|
||||
pack.slug = Json::requireString(technicPackObject, "slug");
|
||||
if (pack.slug == "vanilla")
|
||||
continue;
|
||||
|
||||
auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
|
||||
if(rawURL == "null") {
|
||||
pack.logoUrl = "null";
|
||||
pack.logoName = "null";
|
||||
}
|
||||
else {
|
||||
pack.logoUrl = rawURL;
|
||||
pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
|
||||
}
|
||||
pack.broken = false;
|
||||
newList.append(pack);
|
||||
}
|
||||
}
|
||||
catch (const JSONValidationError &err)
|
||||
{
|
||||
qCritical() << "Couldn't parse technic search results:" << err.cause() ;
|
||||
return;
|
||||
}
|
||||
searchState = Finished;
|
||||
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
|
||||
modpacks.append(newList);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback)
|
||||
{
|
||||
if(m_logoMap.contains(logo))
|
||||
{
|
||||
callback(ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath());
|
||||
}
|
||||
else
|
||||
{
|
||||
requestLogo(logo, logoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void Technic::ListModel::searchRequestFailed()
|
||||
{
|
||||
jobPtr.reset();
|
||||
|
||||
if(searchState == ResetRequested)
|
||||
{
|
||||
beginResetModel();
|
||||
modpacks.clear();
|
||||
endResetModel();
|
||||
|
||||
performSearch();
|
||||
}
|
||||
else
|
||||
{
|
||||
searchState = Finished;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Technic::ListModel::logoLoaded(QString logo, QString out)
|
||||
{
|
||||
m_loadingLogos.removeAll(logo);
|
||||
m_logoMap.insert(logo, QIcon(out));
|
||||
for(int i = 0; i < modpacks.size(); i++)
|
||||
{
|
||||
if(modpacks[i].logoName == logo)
|
||||
{
|
||||
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Technic::ListModel::logoFailed(QString logo)
|
||||
{
|
||||
m_failedLogos.append(logo);
|
||||
m_loadingLogos.removeAll(logo);
|
||||
}
|
||||
|
||||
void Technic::ListModel::requestLogo(QString logo, QString url)
|
||||
{
|
||||
if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MetaEntryPtr entry = ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
|
||||
NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo));
|
||||
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
|
||||
|
||||
auto fullPath = entry->getFullPath();
|
||||
|
||||
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
|
||||
{
|
||||
logoLoaded(logo, fullPath);
|
||||
});
|
||||
|
||||
QObject::connect(job, &NetJob::failed, this, [this, logo]
|
||||
{
|
||||
logoFailed(logo);
|
||||
});
|
||||
|
||||
job->start();
|
||||
|
||||
m_loadingLogos.append(logo);
|
||||
}
|
||||
70
application/pages/modplatform/technic/TechnicModel.h
Normal file
70
application/pages/modplatform/technic/TechnicModel.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/* Copyright 2020 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 <QModelIndex>
|
||||
|
||||
#include "TechnicData.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
namespace Technic {
|
||||
|
||||
typedef std::function<void(QString)> LogoCallback;
|
||||
|
||||
class ListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ListModel(QObject *parent);
|
||||
virtual ~ListModel();
|
||||
|
||||
virtual QVariant data(const QModelIndex& index, int role) const;
|
||||
virtual int columnCount(const QModelIndex& parent) const;
|
||||
virtual int rowCount(const QModelIndex& parent) const;
|
||||
|
||||
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
|
||||
void searchWithTerm(const QString & term);
|
||||
|
||||
private slots:
|
||||
void searchRequestFinished();
|
||||
void searchRequestFailed();
|
||||
|
||||
void logoFailed(QString logo);
|
||||
void logoLoaded(QString logo, QString out);
|
||||
|
||||
private:
|
||||
void performSearch();
|
||||
void requestLogo(QString logo, QString url);
|
||||
|
||||
private:
|
||||
QList<Modpack> modpacks;
|
||||
QStringList m_failedLogos;
|
||||
QStringList m_loadingLogos;
|
||||
QMap<QString, QIcon> m_logoMap;
|
||||
QMap<QString, LogoCallback> waitingCallbacks;
|
||||
|
||||
QString currentSearchTerm;
|
||||
enum SearchState {
|
||||
None,
|
||||
ResetRequested,
|
||||
Finished
|
||||
} searchState = None;
|
||||
NetJobPtr jobPtr;
|
||||
QByteArray response;
|
||||
};
|
||||
|
||||
}
|
||||
203
application/pages/modplatform/technic/TechnicPage.cpp
Normal file
203
application/pages/modplatform/technic/TechnicPage.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
/* Copyright 2013-2020 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 "TechnicPage.h"
|
||||
#include "ui_TechnicPage.h"
|
||||
|
||||
#include "MultiMC.h"
|
||||
#include "dialogs/NewInstanceDialog.h"
|
||||
#include "TechnicModel.h"
|
||||
#include <QKeyEvent>
|
||||
#include "modplatform/technic/SingleZipPackInstallTask.h"
|
||||
#include "modplatform/technic/SolderPackInstallTask.h"
|
||||
#include "Json.h"
|
||||
|
||||
TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
|
||||
: QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch);
|
||||
ui->searchEdit->installEventFilter(this);
|
||||
model = new Technic::ListModel(this);
|
||||
ui->packView->setModel(model);
|
||||
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);
|
||||
}
|
||||
|
||||
bool TechnicPage::eventFilter(QObject* watched, QEvent* event)
|
||||
{
|
||||
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
|
||||
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
||||
if (keyEvent->key() == Qt::Key_Return) {
|
||||
triggerSearch();
|
||||
keyEvent->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
TechnicPage::~TechnicPage()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
bool TechnicPage::shouldDisplay() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void TechnicPage::openedImpl()
|
||||
{
|
||||
dialog->setSuggestedPack();
|
||||
}
|
||||
|
||||
void TechnicPage::triggerSearch() {
|
||||
model->searchWithTerm(ui->searchEdit->text());
|
||||
}
|
||||
|
||||
void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second)
|
||||
{
|
||||
if(!first.isValid())
|
||||
{
|
||||
if(isOpened)
|
||||
{
|
||||
dialog->setSuggestedPack();
|
||||
}
|
||||
//ui->frame->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
current = model->data(first, Qt::UserRole).value<Technic::Modpack>();
|
||||
suggestCurrent();
|
||||
}
|
||||
|
||||
void TechnicPage::suggestCurrent()
|
||||
{
|
||||
if (!isOpened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (current.broken)
|
||||
{
|
||||
dialog->setSuggestedPack();
|
||||
return;
|
||||
}
|
||||
|
||||
QString editedLogoName;
|
||||
editedLogoName = "technic_" + current.logoName.section(".", 0, 0);
|
||||
model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
|
||||
{
|
||||
dialog->setSuggestedIconFromFile(logo, editedLogoName);
|
||||
});
|
||||
|
||||
if (current.metadataLoaded)
|
||||
{
|
||||
metadataLoaded();
|
||||
}
|
||||
else
|
||||
{
|
||||
NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name));
|
||||
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
|
||||
QString slug = current.slug;
|
||||
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get()));
|
||||
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug]
|
||||
{
|
||||
if (current.slug != slug)
|
||||
{
|
||||
return;
|
||||
}
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
QJsonObject obj = doc.object();
|
||||
if(parse_error.error != QJsonParseError::NoError)
|
||||
{
|
||||
qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
}
|
||||
if (!obj.contains("url"))
|
||||
{
|
||||
qWarning() << "Json doesn't contain an url key";
|
||||
return;
|
||||
}
|
||||
QJsonValueRef url = obj["url"];
|
||||
if (url.isString())
|
||||
{
|
||||
current.url = url.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!obj.contains("solder"))
|
||||
{
|
||||
qWarning() << "Json doesn't contain a valid url or solder key";
|
||||
return;
|
||||
}
|
||||
QJsonValueRef solderUrl = obj["solder"];
|
||||
if (solderUrl.isString())
|
||||
{
|
||||
current.url = solderUrl.toString();
|
||||
current.isSolder = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Json doesn't contain a valid url or solder key";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
|
||||
current.metadataLoaded = true;
|
||||
metadataLoaded();
|
||||
});
|
||||
netJob->start();
|
||||
}
|
||||
}
|
||||
|
||||
// expects current.metadataLoaded to be true
|
||||
void TechnicPage::metadataLoaded()
|
||||
{
|
||||
/*QString text = "";
|
||||
QString name = current.name;
|
||||
|
||||
if (current.websiteUrl.isEmpty())
|
||||
text = name;
|
||||
else
|
||||
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
|
||||
if (!current.authors.empty()) {
|
||||
auto authorToStr = [](Technic::ModpackAuthor & author) {
|
||||
if(author.url.isEmpty()) {
|
||||
return author.name;
|
||||
}
|
||||
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
|
||||
};
|
||||
QStringList authorStrs;
|
||||
for(auto & author: current.authors) {
|
||||
authorStrs.push_back(authorToStr(author));
|
||||
}
|
||||
text += tr(" by ") + authorStrs.join(", ");
|
||||
}
|
||||
|
||||
ui->frame->setModText(text);
|
||||
ui->frame->setModDescription(current.description);*/
|
||||
if (!current.isSolder)
|
||||
{
|
||||
dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
|
||||
}
|
||||
else
|
||||
{
|
||||
while (current.url.endsWith('/')) current.url.chop(1);
|
||||
dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(current.url + "/modpack/" + current.slug, current.minecraftVersion));
|
||||
}
|
||||
}
|
||||
78
application/pages/modplatform/technic/TechnicPage.h
Normal file
78
application/pages/modplatform/technic/TechnicPage.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/* Copyright 2013-2020 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 <QWidget>
|
||||
|
||||
#include "pages/BasePage.h"
|
||||
#include <MultiMC.h>
|
||||
#include "tasks/Task.h"
|
||||
#include "TechnicData.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class TechnicPage;
|
||||
}
|
||||
|
||||
class NewInstanceDialog;
|
||||
|
||||
namespace Technic {
|
||||
class ListModel;
|
||||
}
|
||||
|
||||
class TechnicPage : public QWidget, public BasePage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0);
|
||||
virtual ~TechnicPage();
|
||||
virtual QString displayName() const override
|
||||
{
|
||||
return tr("Technic");
|
||||
}
|
||||
virtual QIcon icon() const override
|
||||
{
|
||||
return MMC->getThemedIcon("technic");
|
||||
}
|
||||
virtual QString id() const override
|
||||
{
|
||||
return "technic";
|
||||
}
|
||||
virtual QString helpPage() const override
|
||||
{
|
||||
return "Technic-platform";
|
||||
}
|
||||
virtual bool shouldDisplay() const override;
|
||||
|
||||
void openedImpl() override;
|
||||
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
|
||||
private:
|
||||
void suggestCurrent();
|
||||
void metadataLoaded();
|
||||
|
||||
private slots:
|
||||
void triggerSearch();
|
||||
void onSelectionChanged(QModelIndex first, QModelIndex second);
|
||||
|
||||
private:
|
||||
Ui::TechnicPage *ui = nullptr;
|
||||
NewInstanceDialog* dialog = nullptr;
|
||||
Technic::ListModel* model = nullptr;
|
||||
Technic::Modpack current;
|
||||
};
|
||||
62
application/pages/modplatform/technic/TechnicPage.ui
Normal file
62
application/pages/modplatform/technic/TechnicPage.ui
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TechnicPage</class>
|
||||
<widget class="QWidget" name="TechnicPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>546</width>
|
||||
<height>405</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="searchEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="searchButton">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="packView">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>48</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -104,7 +104,7 @@ void ListModel::requestLogo(QString logo, QString url)
|
||||
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
|
||||
|
||||
auto fullPath = entry->getFullPath();
|
||||
QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath]
|
||||
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
|
||||
{
|
||||
emit logoLoaded(logo, QIcon(fullPath));
|
||||
if(waitingCallbacks.contains(logo))
|
||||
|
||||
BIN
application/resources/assets/underconstruction.png
Normal file
BIN
application/resources/assets/underconstruction.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2479" viewBox="0 0 1464.248 1452.156"><path fill="#1389D2" d="M644.592 5.192c159.049-19.094 324.188 14.764 461.941 96.756 143.299 84.188 256.41 218.769 313.509 374.945 62.353 168.133 58.63 359.689-10.432 525.205-63.691 154.529-182.775 285.021-330.227 363.417-149.71 80.709-327.849 105.845-494.333 71.979-172.465-34.279-331.022-133.533-437.232-273.729C51.059 1037.725-2.015 878.924.058 719.941c.548-167.949 63.08-334.559 172.343-461.998C290.451 118.177 462.794 25.934 644.592 5.192m-120.67 181.494c-131.408 49.538-243.479 148.488-308.511 273.003-60.641 114.509-80.038 249.94-54.906 377.018 25.076 130.193 97.609 250.192 200.162 334.012 101.76 84.247 232.924 131.896 365.063 132.754-.795-37.831-.366-75.591-1.04-113.416-.792-31.415-30.866-53.744-59.722-58.563-.917 28.305.119 62.895-24.771 82.113-23.731 16.288-55.638 15.919-82.479 8.782-27.149-7.626-42.216-35.808-43.011-62.532-1.647-40.565-.244-81.196-.729-121.828-10.86-.059-21.781-.059-32.702-.059-.06-35.442-.06-70.892 0-106.273 10.92-.065 21.84-.125 32.762-.125 0-12.076 0-24.093-.06-36.115-14.033-4.635-30.263-10.188-35.934-25.379-14.033-33.186 3.417-70.462-10.188-103.891-13.911-35.203-27.759-74.125-14.825-111.888 7.992-26.598 34.042-43.56 61.007-45.694-.06-76.808 0-153.553-.06-230.358-.429-40.754 16.288-82.725 48.924-108.164 37.215-29.525 86.57-37.092 132.812-36.786 102.921.061 203.701 48.864 270.2 126.893-.124 18.362 0 36.727-.124 55.149-92.185.061-184.359-.063-276.54 0-10.006-1.828-19.523 7.505-17.754 17.569.304 58.812-.549 117.682-.308 176.492 17.021.061 34.104.304 51.125-.488 2.866-11.774 6.038-23.425 9.454-35.019 74.982 18.424 149.893 36.789 224.809 55.335-2.502 8.6-4.938 17.266-7.382 25.927-6.588 2.5-13.177 5.001-19.767 7.504-2.621 203.154-4.331 406.363-6.648 609.514 176.251-60.091 319.795-208.828 369.755-388.73 39.415-137.021 25.262-288.622-39.527-415.699-63.628-126.646-175.579-227.98-307.72-279.104-133.783-52.65-287.092-53.321-421.365-1.954m140.741 710.665c0 11.401 0 22.817.063 34.221 10.125.06 20.253.06 30.381.06.061 35.448.061 70.896 0 106.34-10.128 0-20.315 0-30.442.059v32.333c19.949.06 39.961.06 59.909-.061-.917-57.646-.365-115.305-1.283-172.95a9391.232 9391.232 0 0 0-58.628-.002z"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="2546.7" height="2526" version="1.1" viewBox="0 0 1491.6 1479.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path d="m658.25 18.964c159.05-19.094 324.19 14.764 461.94 96.756 143.3 84.188 256.41 218.77 313.51 374.94 62.353 168.13 58.63 359.69-10.432 525.21-63.691 154.53-182.78 285.02-330.23 363.42-149.71 80.709-327.85 105.84-494.33 71.979-172.46-34.279-331.02-133.53-437.23-273.73-96.759-126.04-149.83-284.84-147.76-443.82 0.548-167.95 63.08-334.56 172.34-462 118.05-139.77 290.39-232.01 472.19-252.75m-120.67 181.49c-131.41 49.538-243.48 148.49-308.51 273-60.641 114.51-80.038 249.94-54.906 377.02 25.076 130.19 97.609 250.19 200.16 334.01 101.76 84.247 232.92 131.9 365.06 132.75-0.795-37.831-0.366-75.591-1.04-113.42-0.792-31.415-30.866-53.744-59.722-58.563-0.917 28.305 0.119 62.895-24.771 82.113-23.731 16.288-55.638 15.919-82.479 8.782-27.149-7.626-42.216-35.808-43.011-62.532-1.647-40.565-0.244-81.196-0.729-121.83-10.86-0.059-21.781-0.059-32.702-0.059-0.06-35.442-0.06-70.892 0-106.27 10.92-0.065 21.84-0.125 32.762-0.125 0-12.076 0-24.093-0.06-36.115-14.033-4.635-30.263-10.188-35.934-25.379-14.033-33.186 3.417-70.462-10.188-103.89-13.911-35.203-27.759-74.125-14.825-111.89 7.992-26.598 34.042-43.56 61.007-45.694-0.06-76.808 0-153.55-0.06-230.36-0.429-40.754 16.288-82.725 48.924-108.16 37.215-29.525 86.57-37.092 132.81-36.786 102.92 0.061 203.7 48.864 270.2 126.89-0.124 18.362 0 36.727-0.124 55.149-92.185 0.061-184.36-0.063-276.54 0-10.006-1.828-19.523 7.505-17.754 17.569 0.304 58.812-0.549 117.68-0.308 176.49 17.021 0.061 34.104 0.304 51.125-0.488 2.866-11.774 6.038-23.425 9.454-35.019 74.982 18.424 149.89 36.789 224.81 55.335-2.502 8.6-4.938 17.266-7.382 25.927-6.588 2.5-13.177 5.001-19.767 7.504-2.621 203.15-4.331 406.36-6.648 609.51 176.25-60.091 319.8-208.83 369.76-388.73 39.415-137.02 25.262-288.62-39.527-415.7-63.628-126.65-175.58-227.98-307.72-279.1-133.78-52.65-287.09-53.321-421.36-1.954m140.74 710.66c0 11.401 0 22.817 0.063 34.221 10.125 0.06 20.253 0.06 30.381 0.06 0.061 35.448 0.061 70.896 0 106.34-10.128 0-20.315 0-30.442 0.059v32.333c19.949 0.06 39.961 0.06 59.909-0.061-0.917-57.646-0.365-115.31-1.283-172.95a9391.2 9391.2 0 0 0-58.628 0z" fill="#1389d2" stroke="#000" stroke-width="27.532"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user