Merge pull request #588 from flowln/mod_update
Implement mod updater (😎)
This commit is contained in:
commit
dec81c4f27
@ -323,6 +323,7 @@ add_subdirectory(libraries/optional-bare)
|
||||
add_subdirectory(libraries/tomlc99) # toml parser
|
||||
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
|
||||
add_subdirectory(libraries/gamemode)
|
||||
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
|
||||
|
||||
############################### Built Artifacts ###############################
|
||||
|
||||
|
@ -397,6 +397,8 @@ set(TASKS_SOURCES
|
||||
tasks/ConcurrentTask.cpp
|
||||
tasks/SequentialTask.h
|
||||
tasks/SequentialTask.cpp
|
||||
tasks/MultipleOptionsTask.h
|
||||
tasks/MultipleOptionsTask.cpp
|
||||
)
|
||||
|
||||
ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||
@ -480,9 +482,15 @@ set(API_SOURCES
|
||||
|
||||
modplatform/ModAPI.h
|
||||
|
||||
modplatform/flame/FlameAPI.h
|
||||
modplatform/modrinth/ModrinthAPI.h
|
||||
modplatform/EnsureMetadataTask.h
|
||||
modplatform/EnsureMetadataTask.cpp
|
||||
|
||||
modplatform/CheckUpdateTask.h
|
||||
|
||||
modplatform/flame/FlameAPI.h
|
||||
modplatform/flame/FlameAPI.cpp
|
||||
modplatform/modrinth/ModrinthAPI.h
|
||||
modplatform/modrinth/ModrinthAPI.cpp
|
||||
modplatform/helpers/NetworkModAPI.h
|
||||
modplatform/helpers/NetworkModAPI.cpp
|
||||
)
|
||||
@ -508,6 +516,8 @@ set(FLAME_SOURCES
|
||||
modplatform/flame/PackManifest.cpp
|
||||
modplatform/flame/FileResolvingTask.h
|
||||
modplatform/flame/FileResolvingTask.cpp
|
||||
modplatform/flame/FlameCheckUpdate.cpp
|
||||
modplatform/flame/FlameCheckUpdate.h
|
||||
)
|
||||
|
||||
set(MODRINTH_SOURCES
|
||||
@ -515,6 +525,8 @@ set(MODRINTH_SOURCES
|
||||
modplatform/modrinth/ModrinthPackIndex.h
|
||||
modplatform/modrinth/ModrinthPackManifest.cpp
|
||||
modplatform/modrinth/ModrinthPackManifest.h
|
||||
modplatform/modrinth/ModrinthCheckUpdate.cpp
|
||||
modplatform/modrinth/ModrinthCheckUpdate.h
|
||||
)
|
||||
|
||||
set(MODPACKSCH_SOURCES
|
||||
@ -836,6 +848,10 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/ModDownloadDialog.h
|
||||
ui/dialogs/ScrollMessageBox.cpp
|
||||
ui/dialogs/ScrollMessageBox.h
|
||||
ui/dialogs/ChooseProviderDialog.h
|
||||
ui/dialogs/ChooseProviderDialog.cpp
|
||||
ui/dialogs/ModUpdateDialog.cpp
|
||||
ui/dialogs/ModUpdateDialog.h
|
||||
|
||||
# GUI - widgets
|
||||
ui/widgets/Common.cpp
|
||||
@ -941,6 +957,7 @@ qt_wrap_ui(LAUNCHER_UI
|
||||
ui/dialogs/EditAccountDialog.ui
|
||||
ui/dialogs/ReviewMessageBox.ui
|
||||
ui/dialogs/ScrollMessageBox.ui
|
||||
ui/dialogs/ChooseProviderDialog.ui
|
||||
)
|
||||
|
||||
qt_add_resources(LAUNCHER_RESOURCES
|
||||
@ -967,6 +984,7 @@ add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHE
|
||||
target_link_libraries(Launcher_logic
|
||||
systeminfo
|
||||
Launcher_classparser
|
||||
Launcher_murmur2
|
||||
nbt++
|
||||
${ZLIB_LIBRARIES}
|
||||
optional-bare
|
||||
|
@ -127,7 +127,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
|
||||
}
|
||||
|
||||
// ours
|
||||
bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods)
|
||||
bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
|
||||
{
|
||||
QuaZip zipOut(targetJarPath);
|
||||
if (!zipOut.open(QuaZip::mdCreate))
|
||||
@ -141,42 +141,40 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
||||
QSet<QString> addedFiles;
|
||||
|
||||
// Modify the jar
|
||||
QListIterator<Mod> i(mods);
|
||||
i.toBack();
|
||||
while (i.hasPrevious())
|
||||
for (auto i = mods.constEnd(); i != mods.constBegin(); --i)
|
||||
{
|
||||
const Mod &mod = i.previous();
|
||||
const Mod* mod = *i;
|
||||
// do not merge disabled mods.
|
||||
if (!mod.enabled())
|
||||
if (!mod->enabled())
|
||||
continue;
|
||||
if (mod.type() == Mod::MOD_ZIPFILE)
|
||||
if (mod->type() == Mod::MOD_ZIPFILE)
|
||||
{
|
||||
if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles))
|
||||
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
|
||||
{
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar.";
|
||||
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (mod.type() == Mod::MOD_SINGLEFILE)
|
||||
else if (mod->type() == Mod::MOD_SINGLEFILE)
|
||||
{
|
||||
// FIXME: buggy - does not work with addedFiles
|
||||
auto filename = mod.fileinfo();
|
||||
auto filename = mod->fileinfo();
|
||||
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName()))
|
||||
{
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar.";
|
||||
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
addedFiles.insert(filename.fileName());
|
||||
}
|
||||
else if (mod.type() == Mod::MOD_FOLDER)
|
||||
else if (mod->type() == Mod::MOD_FOLDER)
|
||||
{
|
||||
// untested, but seems to be unused / not possible to reach
|
||||
// FIXME: buggy - does not work with addedFiles
|
||||
auto filename = mod.fileinfo();
|
||||
auto filename = mod->fileinfo();
|
||||
QString what_to_zip = filename.absoluteFilePath();
|
||||
QDir dir(what_to_zip);
|
||||
dir.cdUp();
|
||||
@ -193,7 +191,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
||||
{
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar.";
|
||||
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
qDebug() << "Adding folder " << filename.fileName() << " from "
|
||||
@ -204,7 +202,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
||||
// Make sure we do not continue launching when something is missing or undefined...
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
qCritical() << "Failed to add unknown mod type" << mod.fileinfo().fileName() << "to the jar.";
|
||||
qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ namespace MMCZip
|
||||
/**
|
||||
* take a source jar, add mods to it, resulting in target jar
|
||||
*/
|
||||
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods);
|
||||
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
|
||||
|
||||
/**
|
||||
* Find a single file in archive by file name (not path)
|
||||
|
@ -27,6 +27,7 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde
|
||||
{
|
||||
if (is_indexed) {
|
||||
m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version));
|
||||
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod);
|
||||
|
||||
addTask(m_update_task);
|
||||
}
|
||||
@ -40,12 +41,16 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde
|
||||
connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed);
|
||||
|
||||
addTask(m_filesNetJob);
|
||||
|
||||
}
|
||||
|
||||
void ModDownloadTask::downloadSucceeded()
|
||||
{
|
||||
m_filesNetJob.reset();
|
||||
auto name = std::get<0>(to_delete);
|
||||
auto filename = std::get<1>(to_delete);
|
||||
if (!name.isEmpty() && filename != m_mod_version.fileName) {
|
||||
mods->uninstallMod(filename, true);
|
||||
}
|
||||
}
|
||||
|
||||
void ModDownloadTask::downloadFailed(QString reason)
|
||||
@ -58,3 +63,10 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
|
||||
{
|
||||
emit progress(current, total);
|
||||
}
|
||||
|
||||
// This indirection is done so that we don't delete a mod before being sure it was
|
||||
// downloaded successfully!
|
||||
void ModDownloadTask::hasOldMod(QString name, QString filename)
|
||||
{
|
||||
to_delete = {name, filename};
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class ModFolderModel;
|
||||
class ModDownloadTask : public SequentialTask {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed);
|
||||
explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true);
|
||||
const QString& getFilename() const { return m_mod_version.fileName; }
|
||||
|
||||
private:
|
||||
@ -46,6 +46,11 @@ private:
|
||||
void downloadFailed(QString reason);
|
||||
|
||||
void downloadSucceeded();
|
||||
|
||||
std::tuple<QString, QString> to_delete {"", ""};
|
||||
|
||||
private slots:
|
||||
void hasOldMod(QString name, QString filename);
|
||||
};
|
||||
|
||||
|
||||
|
@ -700,24 +700,24 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
|
||||
{
|
||||
out << QString("%1:").arg(label);
|
||||
auto modList = model.allMods();
|
||||
std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) {
|
||||
auto aName = a.fileinfo().completeBaseName();
|
||||
auto bName = b.fileinfo().completeBaseName();
|
||||
std::sort(modList.begin(), modList.end(), [](Mod::Ptr a, Mod::Ptr b) {
|
||||
auto aName = a->fileinfo().completeBaseName();
|
||||
auto bName = b->fileinfo().completeBaseName();
|
||||
return aName.localeAwareCompare(bName) < 0;
|
||||
});
|
||||
for(auto & mod: modList)
|
||||
for(auto mod: modList)
|
||||
{
|
||||
if(mod.type() == Mod::MOD_FOLDER)
|
||||
if(mod->type() == Mod::MOD_FOLDER)
|
||||
{
|
||||
out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)";
|
||||
out << u8" [📁] " + mod->fileinfo().completeBaseName() + " (folder)";
|
||||
continue;
|
||||
}
|
||||
|
||||
if(mod.enabled()) {
|
||||
out << u8" [✔️] " + mod.fileinfo().completeBaseName();
|
||||
if(mod->enabled()) {
|
||||
out << u8" [✔️]" + mod->fileinfo().completeBaseName();
|
||||
}
|
||||
else {
|
||||
out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)";
|
||||
out << u8" [❌] " + mod->fileinfo().completeBaseName() + " (disabled)";
|
||||
}
|
||||
|
||||
}
|
||||
@ -1136,16 +1136,16 @@ std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const
|
||||
return m_game_options;
|
||||
}
|
||||
|
||||
QList< Mod > MinecraftInstance::getJarMods() const
|
||||
QList<Mod*> MinecraftInstance::getJarMods() const
|
||||
{
|
||||
auto profile = m_components->getProfile();
|
||||
QList<Mod> mods;
|
||||
QList<Mod*> mods;
|
||||
for (auto jarmod : profile->getJarMods())
|
||||
{
|
||||
QStringList jar, temp1, temp2, temp3;
|
||||
jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath());
|
||||
// QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem));
|
||||
mods.push_back(Mod(QFileInfo(jar[0])));
|
||||
mods.push_back(new Mod(QFileInfo(jar[0])));
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ public:
|
||||
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
|
||||
QStringList extraArguments() const override;
|
||||
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
|
||||
QList<Mod> getJarMods() const;
|
||||
QList<Mod*> getJarMods() const;
|
||||
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
|
||||
/// get arguments passed to java
|
||||
QStringList javaArguments() const;
|
||||
|
@ -37,9 +37,9 @@ class Metadata {
|
||||
return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version);
|
||||
}
|
||||
|
||||
static auto create(QDir& index_dir, Mod& internal_mod) -> ModStruct
|
||||
static auto create(QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct
|
||||
{
|
||||
return Packwiz::V1::createModFormat(index_dir, internal_mod);
|
||||
return Packwiz::V1::createModFormat(index_dir, internal_mod, mod_slug);
|
||||
}
|
||||
|
||||
static void update(QDir& index_dir, ModStruct& mod)
|
||||
@ -47,13 +47,23 @@ class Metadata {
|
||||
Packwiz::V1::updateModIndex(index_dir, mod);
|
||||
}
|
||||
|
||||
static void remove(QDir& index_dir, QString& mod_name)
|
||||
static void remove(QDir& index_dir, QString mod_slug)
|
||||
{
|
||||
Packwiz::V1::deleteModIndex(index_dir, mod_name);
|
||||
Packwiz::V1::deleteModIndex(index_dir, mod_slug);
|
||||
}
|
||||
|
||||
static auto get(QDir& index_dir, QString& mod_name) -> ModStruct
|
||||
static void remove(QDir& index_dir, QVariant& mod_id)
|
||||
{
|
||||
return Packwiz::V1::getIndexForMod(index_dir, mod_name);
|
||||
Packwiz::V1::deleteModIndex(index_dir, mod_id);
|
||||
}
|
||||
|
||||
static auto get(QDir& index_dir, QString mod_slug) -> ModStruct
|
||||
{
|
||||
return Packwiz::V1::getIndexForMod(index_dir, mod_slug);
|
||||
}
|
||||
|
||||
static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct
|
||||
{
|
||||
return Packwiz::V1::getIndexForMod(index_dir, mod_id);
|
||||
}
|
||||
};
|
||||
|
@ -150,25 +150,30 @@ void Mod::setStatus(ModStatus status)
|
||||
m_temp_status = status;
|
||||
}
|
||||
}
|
||||
void Mod::setMetadata(Metadata::ModStruct* metadata)
|
||||
void Mod::setMetadata(const Metadata::ModStruct& metadata)
|
||||
{
|
||||
if (status() == ModStatus::NoMetadata)
|
||||
setStatus(ModStatus::Installed);
|
||||
|
||||
if (m_localDetails) {
|
||||
m_localDetails->metadata.reset(metadata);
|
||||
m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
||||
} else {
|
||||
m_temp_metadata.reset(metadata);
|
||||
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
||||
}
|
||||
}
|
||||
|
||||
auto Mod::destroy(QDir& index_dir) -> bool
|
||||
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
|
||||
{
|
||||
auto n = name();
|
||||
// FIXME: This can fail to remove the metadata if the
|
||||
// "ModMetadataDisabled" setting is on, since there could
|
||||
// be a name mismatch!
|
||||
Metadata::remove(index_dir, n);
|
||||
if (!preserve_metadata) {
|
||||
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
|
||||
|
||||
if (metadata()) {
|
||||
Metadata::remove(index_dir, metadata()->slug);
|
||||
} else {
|
||||
auto n = name();
|
||||
Metadata::remove(index_dir, n);
|
||||
}
|
||||
}
|
||||
|
||||
m_type = MOD_UNKNOWN;
|
||||
return FS::deletePath(m_file.filePath());
|
||||
@ -182,9 +187,12 @@ auto Mod::details() const -> const ModDetails&
|
||||
auto Mod::name() const -> QString
|
||||
{
|
||||
auto d_name = details().name;
|
||||
if (!d_name.isEmpty()) {
|
||||
if (!d_name.isEmpty())
|
||||
return d_name;
|
||||
}
|
||||
|
||||
if (metadata())
|
||||
return metadata()->name;
|
||||
|
||||
return m_name;
|
||||
}
|
||||
|
||||
@ -235,11 +243,10 @@ void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
|
||||
m_resolved = true;
|
||||
m_localDetails = details;
|
||||
|
||||
if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
|
||||
m_localDetails->metadata = m_temp_metadata;
|
||||
if (status() == ModStatus::NoMetadata)
|
||||
setStatus(ModStatus::Installed);
|
||||
}
|
||||
|
||||
setStatus(m_temp_status);
|
||||
|
||||
if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
|
||||
setMetadata(*m_temp_metadata);
|
||||
m_temp_metadata.reset();
|
||||
}
|
||||
}
|
||||
|
@ -39,10 +39,12 @@
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "ModDetails.h"
|
||||
|
||||
class Mod
|
||||
class Mod : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum ModType
|
||||
{
|
||||
@ -53,6 +55,8 @@ public:
|
||||
MOD_LITEMOD, //!< The mod is a litemod
|
||||
};
|
||||
|
||||
using Ptr = shared_qobject_ptr<Mod>;
|
||||
|
||||
Mod() = default;
|
||||
Mod(const QFileInfo &file);
|
||||
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
|
||||
@ -77,12 +81,12 @@ public:
|
||||
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
|
||||
|
||||
void setStatus(ModStatus status);
|
||||
void setMetadata(Metadata::ModStruct* metadata);
|
||||
void setMetadata(const Metadata::ModStruct& metadata);
|
||||
|
||||
auto enable(bool value) -> bool;
|
||||
|
||||
// delete all the files of this mod
|
||||
auto destroy(QDir& index_dir) -> bool;
|
||||
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
|
||||
|
||||
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
|
||||
void repath(const QFileInfo &file);
|
||||
@ -111,7 +115,7 @@ protected:
|
||||
std::shared_ptr<Metadata::ModStruct> m_temp_metadata;
|
||||
|
||||
/* Set the mod status while it doesn't have local details just yet */
|
||||
ModStatus m_temp_status = ModStatus::NotInstalled;
|
||||
ModStatus m_temp_status = ModStatus::NoMetadata;
|
||||
|
||||
std::shared_ptr<ModDetails> m_localDetails;
|
||||
|
||||
|
@ -65,15 +65,21 @@ void ModFolderModel::startWatching()
|
||||
|
||||
update();
|
||||
|
||||
// Watch the mods folder
|
||||
is_watching = m_watcher->addPath(m_dir.absolutePath());
|
||||
if (is_watching)
|
||||
{
|
||||
if (is_watching) {
|
||||
qDebug() << "Started watching " << m_dir.absolutePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
qDebug() << "Failed to start watching " << m_dir.absolutePath();
|
||||
}
|
||||
|
||||
// Watch the mods index folder
|
||||
is_watching = m_watcher->addPath(indexDir().absolutePath());
|
||||
if (is_watching) {
|
||||
qDebug() << "Started watching " << indexDir().absolutePath();
|
||||
} else {
|
||||
qDebug() << "Failed to start watching " << indexDir().absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
void ModFolderModel::stopWatching()
|
||||
@ -82,14 +88,18 @@ void ModFolderModel::stopWatching()
|
||||
return;
|
||||
|
||||
is_watching = !m_watcher->removePath(m_dir.absolutePath());
|
||||
if (!is_watching)
|
||||
{
|
||||
if (!is_watching) {
|
||||
qDebug() << "Stopped watching " << m_dir.absolutePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
|
||||
}
|
||||
|
||||
is_watching = !m_watcher->removePath(indexDir().absolutePath());
|
||||
if (!is_watching) {
|
||||
qDebug() << "Stopped watching " << indexDir().absolutePath();
|
||||
} else {
|
||||
qDebug() << "Failed to stop watching " << indexDir().absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
bool ModFolderModel::update()
|
||||
@ -124,7 +134,7 @@ void ModFolderModel::finishUpdate()
|
||||
QSet<QString> newSet(newList.begin(), newList.end());
|
||||
#else
|
||||
QSet<QString> currentSet = modsIndex.keys().toSet();
|
||||
auto & newMods = m_update->mods;
|
||||
auto& newMods = m_update->mods;
|
||||
QSet<QString> newSet = newMods.keys().toSet();
|
||||
#endif
|
||||
|
||||
@ -132,19 +142,20 @@ void ModFolderModel::finishUpdate()
|
||||
{
|
||||
QSet<QString> kept = currentSet;
|
||||
kept.intersect(newSet);
|
||||
for(auto & keptMod: kept) {
|
||||
auto & newMod = newMods[keptMod];
|
||||
for(auto& keptMod : kept) {
|
||||
auto newMod = newMods[keptMod];
|
||||
auto row = modsIndex[keptMod];
|
||||
auto & currentMod = mods[row];
|
||||
if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) {
|
||||
auto currentMod = mods[row];
|
||||
if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
|
||||
// no significant change, ignore...
|
||||
continue;
|
||||
}
|
||||
auto & oldMod = mods[row];
|
||||
if(oldMod.isResolving()) {
|
||||
activeTickets.remove(oldMod.resolutionTicket());
|
||||
auto oldMod = mods[row];
|
||||
if(oldMod->isResolving()) {
|
||||
activeTickets.remove(oldMod->resolutionTicket());
|
||||
}
|
||||
oldMod = newMod;
|
||||
|
||||
mods[row] = newMod;
|
||||
resolveMod(mods[row]);
|
||||
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
|
||||
}
|
||||
@ -163,9 +174,10 @@ void ModFolderModel::finishUpdate()
|
||||
int removedIndex = *iter;
|
||||
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
|
||||
auto removedIter = mods.begin() + removedIndex;
|
||||
if(removedIter->isResolving()) {
|
||||
activeTickets.remove(removedIter->resolutionTicket());
|
||||
if((*removedIter)->isResolving()) {
|
||||
activeTickets.remove((*removedIter)->resolutionTicket());
|
||||
}
|
||||
|
||||
mods.erase(removedIter);
|
||||
endRemoveRows();
|
||||
}
|
||||
@ -191,8 +203,8 @@ void ModFolderModel::finishUpdate()
|
||||
{
|
||||
modsIndex.clear();
|
||||
int idx = 0;
|
||||
for(auto & mod: mods) {
|
||||
modsIndex[mod.internal_id()] = idx;
|
||||
for(auto mod: mods) {
|
||||
modsIndex[mod->internal_id()] = idx;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
@ -207,17 +219,17 @@ void ModFolderModel::finishUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
void ModFolderModel::resolveMod(Mod& m)
|
||||
void ModFolderModel::resolveMod(Mod::Ptr m)
|
||||
{
|
||||
if(!m.shouldResolve()) {
|
||||
if(!m->shouldResolve()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo());
|
||||
auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
|
||||
auto result = task->result();
|
||||
result->id = m.internal_id();
|
||||
result->id = m->internal_id();
|
||||
activeTickets.insert(nextResolutionTicket, result);
|
||||
m.setResolving(true, nextResolutionTicket);
|
||||
m->setResolving(true, nextResolutionTicket);
|
||||
nextResolutionTicket++;
|
||||
QThreadPool *threadPool = QThreadPool::globalInstance();
|
||||
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
|
||||
@ -233,8 +245,8 @@ void ModFolderModel::finishModParse(int token)
|
||||
auto result = *iter;
|
||||
activeTickets.remove(token);
|
||||
int row = modsIndex[result->id];
|
||||
auto & mod = mods[row];
|
||||
mod.finishResolvingWithDetails(result->details);
|
||||
auto mod = mods[row];
|
||||
mod->finishResolvingWithDetails(result->details);
|
||||
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
|
||||
}
|
||||
|
||||
@ -259,6 +271,18 @@ bool ModFolderModel::isValid()
|
||||
return m_dir.exists() && m_dir.isReadable();
|
||||
}
|
||||
|
||||
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
|
||||
{
|
||||
QList<Mod::Ptr> selected_mods;
|
||||
for (auto i : indexes) {
|
||||
if(i.column() != 0)
|
||||
continue;
|
||||
|
||||
selected_mods.push_back(mods[i.row()]);
|
||||
}
|
||||
return selected_mods;
|
||||
}
|
||||
|
||||
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
|
||||
bool ModFolderModel::installMod(const QString &filename)
|
||||
{
|
||||
@ -344,6 +368,20 @@ bool ModFolderModel::installMod(const QString &filename)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
|
||||
{
|
||||
|
||||
for(auto mod : allMods()){
|
||||
if(mod->fileinfo().fileName() == filename){
|
||||
auto index_dir = indexDir();
|
||||
mod->destroy(index_dir, preserve_metadata);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
|
||||
{
|
||||
if(interaction_disabled) {
|
||||
@ -377,9 +415,9 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
|
||||
if(i.column() != 0) {
|
||||
continue;
|
||||
}
|
||||
Mod &m = mods[i.row()];
|
||||
auto m = mods[i.row()];
|
||||
auto index_dir = indexDir();
|
||||
m.destroy(index_dir);
|
||||
m->destroy(index_dir);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -406,9 +444,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
||||
switch (column)
|
||||
{
|
||||
case NameColumn:
|
||||
return mods[row].name();
|
||||
return mods[row]->name();
|
||||
case VersionColumn: {
|
||||
switch(mods[row].type()) {
|
||||
switch(mods[row]->type()) {
|
||||
case Mod::MOD_FOLDER:
|
||||
return tr("Folder");
|
||||
case Mod::MOD_SINGLEFILE:
|
||||
@ -416,23 +454,23 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return mods[row].version();
|
||||
return mods[row]->version();
|
||||
}
|
||||
case DateColumn:
|
||||
return mods[row].dateTimeChanged();
|
||||
return mods[row]->dateTimeChanged();
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
return mods[row].internal_id();
|
||||
return mods[row]->internal_id();
|
||||
|
||||
case Qt::CheckStateRole:
|
||||
switch (column)
|
||||
{
|
||||
case ActiveColumn:
|
||||
return mods[row].enabled() ? Qt::Checked : Qt::Unchecked;
|
||||
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@ -472,20 +510,20 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio
|
||||
break;
|
||||
case Toggle:
|
||||
default:
|
||||
desiredStatus = !mod.enabled();
|
||||
desiredStatus = !mod->enabled();
|
||||
break;
|
||||
}
|
||||
|
||||
if(desiredStatus == mod.enabled()) {
|
||||
if(desiredStatus == mod->enabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// preserve the row, but change its ID
|
||||
auto oldId = mod.internal_id();
|
||||
if(!mod.enable(!mod.enabled())) {
|
||||
auto oldId = mod->internal_id();
|
||||
if(!mod->enable(!mod->enabled())) {
|
||||
return false;
|
||||
}
|
||||
auto newId = mod.internal_id();
|
||||
auto newId = mod->internal_id();
|
||||
if(modsIndex.contains(newId)) {
|
||||
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
|
||||
// But is it necessary?
|
||||
|
@ -101,13 +101,13 @@ public:
|
||||
{
|
||||
return size() == 0;
|
||||
}
|
||||
Mod &operator[](size_t index)
|
||||
Mod& operator[](size_t index)
|
||||
{
|
||||
return mods[index];
|
||||
return *mods[index];
|
||||
}
|
||||
const Mod &at(size_t index) const
|
||||
const Mod& at(size_t index) const
|
||||
{
|
||||
return mods.at(index);
|
||||
return *mods.at(index);
|
||||
}
|
||||
|
||||
/// Reloads the mod list and returns true if the list changed.
|
||||
@ -118,6 +118,8 @@ public:
|
||||
*/
|
||||
bool installMod(const QString& filename);
|
||||
|
||||
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
|
||||
|
||||
/// Deletes all the selected mods
|
||||
bool deleteMods(const QModelIndexList &indexes);
|
||||
|
||||
@ -139,11 +141,13 @@ public:
|
||||
return { QString("%1/.index").arg(dir().absolutePath()) };
|
||||
}
|
||||
|
||||
const QList<Mod> & allMods()
|
||||
const QList<Mod::Ptr>& allMods()
|
||||
{
|
||||
return mods;
|
||||
}
|
||||
|
||||
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
|
||||
|
||||
public slots:
|
||||
void disableInteraction(bool disabled);
|
||||
|
||||
@ -157,7 +161,7 @@ signals:
|
||||
void updateFinished();
|
||||
|
||||
private:
|
||||
void resolveMod(Mod& m);
|
||||
void resolveMod(Mod::Ptr m);
|
||||
bool setModStatus(int index, ModStatusAction action);
|
||||
|
||||
protected:
|
||||
@ -171,5 +175,5 @@ protected:
|
||||
QMap<QString, int> modsIndex;
|
||||
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
|
||||
int nextResolutionTicket = 0;
|
||||
QList<Mod> mods;
|
||||
QList<Mod::Ptr> mods;
|
||||
};
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QString>
|
||||
#include <quazip/quazip.h>
|
||||
#include <quazip/quazipfile.h>
|
||||
#include <toml.h>
|
||||
@ -71,7 +72,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
||||
if(val.isUndefined()) {
|
||||
val = jsonDoc.object().value("modListVersion");
|
||||
}
|
||||
int version = val.toDouble();
|
||||
|
||||
int version = Json::ensureInteger(val, -1);
|
||||
|
||||
// Some mods set the number with "", so it's a String instead
|
||||
if (version < 0)
|
||||
version = Json::ensureString(val, "").toInt();
|
||||
|
||||
if (version != 2)
|
||||
{
|
||||
qCritical() << "BAD stuff happened to mod json:";
|
||||
|
@ -44,10 +44,21 @@ void LocalModUpdateTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name));
|
||||
|
||||
auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version);
|
||||
Metadata::update(m_index_dir, pw_mod);
|
||||
auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId);
|
||||
if (old_metadata.isValid()) {
|
||||
emit hasOldMod(old_metadata.name, old_metadata.filename);
|
||||
if (m_mod.slug.isEmpty())
|
||||
m_mod.slug = old_metadata.slug;
|
||||
}
|
||||
|
||||
emitSucceeded();
|
||||
auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version);
|
||||
if (pw_mod.isValid()) {
|
||||
Metadata::update(m_index_dir, pw_mod);
|
||||
emitSucceeded();
|
||||
} else {
|
||||
qCritical() << "Tried to update an invalid mod!";
|
||||
emitFailed(tr("Invalid metadata"));
|
||||
}
|
||||
}
|
||||
|
||||
auto LocalModUpdateTask::abort() -> bool
|
||||
|
@ -37,6 +37,9 @@ class LocalModUpdateTask : public Task {
|
||||
//! Entry point for tasks.
|
||||
void executeTask() override;
|
||||
|
||||
signals:
|
||||
void hasOldMod(QString name, QString filename);
|
||||
|
||||
private:
|
||||
QDir m_index_dir;
|
||||
ModPlatform::IndexedPack& m_mod;
|
||||
|
@ -36,7 +36,6 @@
|
||||
|
||||
#include "ModFolderLoadTask.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "minecraft/mod/MetadataHandler.h"
|
||||
|
||||
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed)
|
||||
@ -53,33 +52,33 @@ void ModFolderLoadTask::run()
|
||||
// Read JAR files that don't have metadata
|
||||
m_mods_dir.refresh();
|
||||
for (auto entry : m_mods_dir.entryInfoList()) {
|
||||
Mod mod(entry);
|
||||
Mod::Ptr mod(new Mod(entry));
|
||||
|
||||
if (mod.enabled()) {
|
||||
if (m_result->mods.contains(mod.internal_id())) {
|
||||
m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed);
|
||||
if (mod->enabled()) {
|
||||
if (m_result->mods.contains(mod->internal_id())) {
|
||||
m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed);
|
||||
}
|
||||
else {
|
||||
m_result->mods[mod.internal_id()] = mod;
|
||||
m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata);
|
||||
m_result->mods[mod->internal_id()] = mod;
|
||||
m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata);
|
||||
}
|
||||
}
|
||||
else {
|
||||
QString chopped_id = mod.internal_id().chopped(9);
|
||||
QString chopped_id = mod->internal_id().chopped(9);
|
||||
if (m_result->mods.contains(chopped_id)) {
|
||||
m_result->mods[mod.internal_id()] = mod;
|
||||
m_result->mods[mod->internal_id()] = mod;
|
||||
|
||||
auto metadata = m_result->mods[chopped_id].metadata();
|
||||
auto metadata = m_result->mods[chopped_id]->metadata();
|
||||
if (metadata) {
|
||||
mod.setMetadata(new Metadata::ModStruct(*metadata));
|
||||
mod->setMetadata(*metadata);
|
||||
|
||||
m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed);
|
||||
m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed);
|
||||
m_result->mods.remove(chopped_id);
|
||||
}
|
||||
}
|
||||
else {
|
||||
m_result->mods[mod.internal_id()] = mod;
|
||||
m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata);
|
||||
m_result->mods[mod->internal_id()] = mod;
|
||||
m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -97,8 +96,8 @@ void ModFolderLoadTask::getFromMetadata()
|
||||
return;
|
||||
}
|
||||
|
||||
Mod mod(m_mods_dir, metadata);
|
||||
mod.setStatus(ModStatus::NotInstalled);
|
||||
m_result->mods[mod.internal_id()] = mod;
|
||||
auto* mod = new Mod(m_mods_dir, metadata);
|
||||
mod->setStatus(ModStatus::NotInstalled);
|
||||
m_result->mods[mod->internal_id()] = mod;
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ class ModFolderLoadTask : public QObject, public QRunnable
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct Result {
|
||||
QMap<QString, Mod> mods;
|
||||
QMap<QString, Mod::Ptr> mods;
|
||||
};
|
||||
using ResultPtr = std::shared_ptr<Result>;
|
||||
ResultPtr result() const {
|
||||
|
51
launcher/modplatform/CheckUpdateTask.h
Normal file
51
launcher/modplatform/CheckUpdateTask.h
Normal file
@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class ModDownloadTask;
|
||||
class ModFolderModel;
|
||||
|
||||
class CheckUpdateTask : public Task {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
|
||||
: Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {};
|
||||
|
||||
struct UpdatableMod {
|
||||
QString name;
|
||||
QString old_hash;
|
||||
QString old_version;
|
||||
QString new_version;
|
||||
QString changelog;
|
||||
ModPlatform::Provider provider;
|
||||
ModDownloadTask* download;
|
||||
|
||||
public:
|
||||
UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t)
|
||||
: name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
|
||||
{}
|
||||
};
|
||||
|
||||
auto getUpdatable() -> std::vector<UpdatableMod>&& { return std::move(m_updatable); }
|
||||
|
||||
public slots:
|
||||
bool abort() override = 0;
|
||||
|
||||
protected slots:
|
||||
void executeTask() override = 0;
|
||||
|
||||
signals:
|
||||
void checkFailed(Mod* failed, QString reason, QUrl recover_url = {});
|
||||
|
||||
protected:
|
||||
QList<Mod*>& m_mods;
|
||||
std::list<Version>& m_game_versions;
|
||||
ModAPI::ModLoaderTypes m_loaders;
|
||||
std::shared_ptr<ModFolderModel> m_mods_folder;
|
||||
|
||||
std::vector<UpdatableMod> m_updatable;
|
||||
};
|
534
launcher/modplatform/EnsureMetadataTask.cpp
Normal file
534
launcher/modplatform/EnsureMetadataTask.cpp
Normal file
@ -0,0 +1,534 @@
|
||||
#include "EnsureMetadataTask.h"
|
||||
|
||||
#include <MurmurHash2.h>
|
||||
#include <QDebug>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
||||
#include "modplatform/flame/FlameAPI.h"
|
||||
#include "modplatform/flame/FlameModIndex.h"
|
||||
#include "modplatform/modrinth/ModrinthAPI.h"
|
||||
#include "modplatform/modrinth/ModrinthPackIndex.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "tasks/MultipleOptionsTask.h"
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
static ModrinthAPI modrinth_api;
|
||||
static FlameAPI flame_api;
|
||||
|
||||
EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov)
|
||||
{
|
||||
auto hash = getHash(mod);
|
||||
if (hash.isEmpty())
|
||||
emitFail(mod);
|
||||
else
|
||||
m_mods.insert(hash, mod);
|
||||
}
|
||||
|
||||
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
|
||||
: Task(nullptr), m_index_dir(dir), m_provider(prov)
|
||||
{
|
||||
for (auto* mod : mods) {
|
||||
if (!mod->valid()) {
|
||||
emitFail(mod);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto hash = getHash(mod);
|
||||
if (hash.isEmpty()) {
|
||||
emitFail(mod);
|
||||
continue;
|
||||
}
|
||||
|
||||
m_mods.insert(hash, mod);
|
||||
}
|
||||
}
|
||||
|
||||
QString EnsureMetadataTask::getHash(Mod* mod)
|
||||
{
|
||||
/* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */
|
||||
QByteArray jar_data;
|
||||
try {
|
||||
jar_data = FS::read(mod->fileinfo().absoluteFilePath());
|
||||
} catch (FS::FileSystemException& e) {
|
||||
qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
|
||||
qCritical() << QString("Reason: ") << e.cause();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (m_provider) {
|
||||
case ModPlatform::Provider::MODRINTH: {
|
||||
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
||||
|
||||
return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
|
||||
}
|
||||
case ModPlatform::Provider::FLAME: {
|
||||
QByteArray jar_data_treated;
|
||||
for (char c : jar_data) {
|
||||
// CF-specific
|
||||
if (!(c == 9 || c == 10 || c == 13 || c == 32))
|
||||
jar_data_treated.push_back(c);
|
||||
}
|
||||
|
||||
return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool EnsureMetadataTask::abort()
|
||||
{
|
||||
// Prevent sending signals to a dead object
|
||||
disconnect(this, 0, 0, 0);
|
||||
|
||||
if (m_current_task)
|
||||
return m_current_task->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnsureMetadataTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Checking if mods have metadata..."));
|
||||
|
||||
for (auto* mod : m_mods) {
|
||||
if (!mod->valid()) {
|
||||
qDebug() << "Mod" << mod->name() << "is invalid!";
|
||||
emitFail(mod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// They already have the right metadata :o
|
||||
if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) {
|
||||
qDebug() << "Mod" << mod->name() << "already has metadata!";
|
||||
emitReady(mod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Folders don't have metadata
|
||||
if (mod->type() == Mod::MOD_FOLDER) {
|
||||
emitReady(mod);
|
||||
}
|
||||
}
|
||||
|
||||
NetJob::Ptr version_task;
|
||||
|
||||
switch (m_provider) {
|
||||
case (ModPlatform::Provider::MODRINTH):
|
||||
version_task = modrinthVersionsTask();
|
||||
break;
|
||||
case (ModPlatform::Provider::FLAME):
|
||||
version_task = flameVersionsTask();
|
||||
break;
|
||||
}
|
||||
|
||||
auto invalidade_leftover = [this] {
|
||||
QMutableHashIterator<QString, Mod*> mods_iter(m_mods);
|
||||
while (mods_iter.hasNext()) {
|
||||
auto mod = mods_iter.next();
|
||||
emitFail(mod.value());
|
||||
}
|
||||
|
||||
emitSucceeded();
|
||||
};
|
||||
|
||||
connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
|
||||
NetJob::Ptr project_task;
|
||||
|
||||
switch (m_provider) {
|
||||
case (ModPlatform::Provider::MODRINTH):
|
||||
project_task = modrinthProjectsTask();
|
||||
break;
|
||||
case (ModPlatform::Provider::FLAME):
|
||||
project_task = flameProjectsTask();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!project_task) {
|
||||
invalidade_leftover();
|
||||
return;
|
||||
}
|
||||
|
||||
connect(project_task.get(), &Task::finished, this, [=] {
|
||||
invalidade_leftover();
|
||||
project_task->deleteLater();
|
||||
m_current_task = nullptr;
|
||||
});
|
||||
|
||||
m_current_task = project_task.get();
|
||||
project_task->start();
|
||||
});
|
||||
|
||||
connect(version_task.get(), &Task::finished, [=] {
|
||||
version_task->deleteLater();
|
||||
m_current_task = nullptr;
|
||||
});
|
||||
|
||||
if (m_mods.size() > 1)
|
||||
setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider)));
|
||||
else if (!m_mods.empty())
|
||||
setStatus(tr("Requesting metadata information from %1 for '%2'...")
|
||||
.arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
|
||||
|
||||
m_current_task = version_task.get();
|
||||
version_task->start();
|
||||
}
|
||||
|
||||
void EnsureMetadataTask::emitReady(Mod* m)
|
||||
{
|
||||
qDebug() << QString("Generated metadata for %1").arg(m->name());
|
||||
emit metadataReady(m);
|
||||
|
||||
m_mods.remove(getHash(m));
|
||||
}
|
||||
|
||||
void EnsureMetadataTask::emitFail(Mod* m)
|
||||
{
|
||||
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
|
||||
emit metadataFailed(m);
|
||||
|
||||
m_mods.remove(getHash(m));
|
||||
}
|
||||
|
||||
// Modrinth
|
||||
|
||||
NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
|
||||
{
|
||||
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
||||
|
||||
auto* response = new QByteArray();
|
||||
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
|
||||
|
||||
// Prevents unfortunate timings when aborting the task
|
||||
if (!ver_task)
|
||||
return {};
|
||||
|
||||
connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
failed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto entries = Json::requireObject(doc);
|
||||
for (auto& hash : m_mods.keys()) {
|
||||
auto mod = m_mods.find(hash).value();
|
||||
try {
|
||||
auto entry = Json::requireObject(entries, hash);
|
||||
|
||||
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
|
||||
qDebug() << "Getting version for" << mod->name() << "from Modrinth";
|
||||
|
||||
m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << entries;
|
||||
|
||||
emitFail(mod);
|
||||
}
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
return ver_task;
|
||||
}
|
||||
|
||||
NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
|
||||
{
|
||||
QHash<QString, QString> addonIds;
|
||||
for (auto const& data : m_temp_versions)
|
||||
addonIds.insert(data.addonId.toString(), data.hash);
|
||||
|
||||
auto response = new QByteArray();
|
||||
NetJob::Ptr proj_task;
|
||||
|
||||
if (addonIds.isEmpty()) {
|
||||
qWarning() << "No addonId found!";
|
||||
} else if (addonIds.size() == 1) {
|
||||
proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response);
|
||||
} else {
|
||||
proj_task = modrinth_api.getProjects(addonIds.keys(), response);
|
||||
}
|
||||
|
||||
// Prevents unfortunate timings when aborting the task
|
||||
if (!proj_task)
|
||||
return {};
|
||||
|
||||
connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
|
||||
QJsonParseError parse_error{};
|
||||
auto doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
QJsonArray entries;
|
||||
if (addonIds.size() == 1)
|
||||
entries = { doc.object() };
|
||||
else
|
||||
entries = Json::requireArray(doc);
|
||||
|
||||
for (auto entry : entries) {
|
||||
auto entry_obj = Json::requireObject(entry);
|
||||
|
||||
ModPlatform::IndexedPack pack;
|
||||
Modrinth::loadIndexedPack(pack, entry_obj);
|
||||
|
||||
auto hash = addonIds.find(pack.addonId.toString()).value();
|
||||
|
||||
auto mod_iter = m_mods.find(hash);
|
||||
if (mod_iter == m_mods.end()) {
|
||||
qWarning() << "Invalid project id from the API response.";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* mod = mod_iter.value();
|
||||
|
||||
try {
|
||||
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
|
||||
|
||||
modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << entries;
|
||||
|
||||
emitFail(mod);
|
||||
}
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
return proj_task;
|
||||
}
|
||||
|
||||
// Flame
|
||||
NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
|
||||
{
|
||||
auto* response = new QByteArray();
|
||||
|
||||
QList<uint> fingerprints;
|
||||
for (auto& murmur : m_mods.keys()) {
|
||||
fingerprints.push_back(murmur.toUInt());
|
||||
}
|
||||
|
||||
auto ver_task = flame_api.matchFingerprints(fingerprints, response);
|
||||
|
||||
connect(ver_task.get(), &Task::succeeded, this, [this, response] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
failed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto doc_obj = Json::requireObject(doc);
|
||||
auto data_obj = Json::requireObject(doc_obj, "data");
|
||||
auto data_arr = Json::requireArray(data_obj, "exactMatches");
|
||||
|
||||
if (data_arr.isEmpty()) {
|
||||
qWarning() << "No matches found for fingerprint search!";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto match : data_arr) {
|
||||
auto match_obj = Json::ensureObject(match, {});
|
||||
auto file_obj = Json::ensureObject(match_obj, "file", {});
|
||||
|
||||
if (match_obj.isEmpty() || file_obj.isEmpty()) {
|
||||
qWarning() << "Fingerprint match is empty!";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
|
||||
auto mod = m_mods.find(fingerprint);
|
||||
if (mod == m_mods.end()) {
|
||||
qWarning() << "Invalid fingerprint from the API response.";
|
||||
continue;
|
||||
}
|
||||
|
||||
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name()));
|
||||
|
||||
m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
|
||||
}
|
||||
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
return ver_task;
|
||||
}
|
||||
|
||||
NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
|
||||
{
|
||||
QHash<QString, QString> addonIds;
|
||||
for (auto const& hash : m_mods.keys()) {
|
||||
if (m_temp_versions.contains(hash)) {
|
||||
auto const& data = m_temp_versions.find(hash).value();
|
||||
|
||||
auto id_str = data.addonId.toString();
|
||||
if (!id_str.isEmpty())
|
||||
addonIds.insert(data.addonId.toString(), hash);
|
||||
}
|
||||
}
|
||||
|
||||
auto response = new QByteArray();
|
||||
NetJob::Ptr proj_task;
|
||||
|
||||
if (addonIds.isEmpty()) {
|
||||
qWarning() << "No addonId found!";
|
||||
} else if (addonIds.size() == 1) {
|
||||
proj_task = flame_api.getProject(*addonIds.keyBegin(), response);
|
||||
} else {
|
||||
proj_task = flame_api.getProjects(addonIds.keys(), response);
|
||||
}
|
||||
|
||||
// Prevents unfortunate timings when aborting the task
|
||||
if (!proj_task)
|
||||
return {};
|
||||
|
||||
connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
|
||||
QJsonParseError parse_error{};
|
||||
auto doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
QJsonArray entries;
|
||||
if (addonIds.size() == 1)
|
||||
entries = { Json::requireObject(Json::requireObject(doc), "data") };
|
||||
else
|
||||
entries = Json::requireArray(Json::requireObject(doc), "data");
|
||||
|
||||
for (auto entry : entries) {
|
||||
auto entry_obj = Json::requireObject(entry);
|
||||
|
||||
auto id = QString::number(Json::requireInteger(entry_obj, "id"));
|
||||
auto hash = addonIds.find(id).value();
|
||||
auto mod = m_mods.find(hash).value();
|
||||
|
||||
try {
|
||||
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name()));
|
||||
|
||||
ModPlatform::IndexedPack pack;
|
||||
FlameMod::loadIndexedPack(pack, entry_obj);
|
||||
|
||||
flameCallback(pack, m_temp_versions.find(hash).value(), mod);
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << entries;
|
||||
|
||||
emitFail(mod);
|
||||
}
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
return proj_task;
|
||||
}
|
||||
|
||||
void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
|
||||
{
|
||||
// Prevent file name mismatch
|
||||
ver.fileName = mod->fileinfo().fileName();
|
||||
if (ver.fileName.endsWith(".disabled"))
|
||||
ver.fileName.chop(9);
|
||||
|
||||
QDir tmp_index_dir(m_index_dir);
|
||||
|
||||
{
|
||||
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
|
||||
QEventLoop loop;
|
||||
|
||||
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
update_metadata.start();
|
||||
|
||||
if (!update_metadata.isFinished())
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
auto metadata = Metadata::get(tmp_index_dir, pack.slug);
|
||||
if (!metadata.isValid()) {
|
||||
qCritical() << "Failed to generate metadata at last step!";
|
||||
emitFail(mod);
|
||||
return;
|
||||
}
|
||||
|
||||
mod->setMetadata(metadata);
|
||||
|
||||
emitReady(mod);
|
||||
}
|
||||
|
||||
void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
|
||||
{
|
||||
try {
|
||||
// Prevent file name mismatch
|
||||
ver.fileName = mod->fileinfo().fileName();
|
||||
if (ver.fileName.endsWith(".disabled"))
|
||||
ver.fileName.chop(9);
|
||||
|
||||
QDir tmp_index_dir(m_index_dir);
|
||||
|
||||
{
|
||||
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
|
||||
QEventLoop loop;
|
||||
|
||||
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
update_metadata.start();
|
||||
|
||||
if (!update_metadata.isFinished())
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
auto metadata = Metadata::get(tmp_index_dir, pack.slug);
|
||||
if (!metadata.isValid()) {
|
||||
qCritical() << "Failed to generate metadata at last step!";
|
||||
emitFail(mod);
|
||||
return;
|
||||
}
|
||||
|
||||
mod->setMetadata(metadata);
|
||||
|
||||
emitReady(mod);
|
||||
} catch (Json::JsonException& e) {
|
||||
qDebug() << e.cause();
|
||||
|
||||
emitFail(mod);
|
||||
}
|
||||
}
|
54
launcher/modplatform/EnsureMetadataTask.h
Normal file
54
launcher/modplatform/EnsureMetadataTask.h
Normal file
@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModIndex.h"
|
||||
#include "tasks/SequentialTask.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
class Mod;
|
||||
class QDir;
|
||||
class MultipleOptionsTask;
|
||||
|
||||
class EnsureMetadataTask : public Task {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
|
||||
EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
|
||||
|
||||
~EnsureMetadataTask() = default;
|
||||
|
||||
public slots:
|
||||
bool abort() override;
|
||||
protected slots:
|
||||
void executeTask() override;
|
||||
|
||||
private:
|
||||
// FIXME: Move to their own namespace
|
||||
auto modrinthVersionsTask() -> NetJob::Ptr;
|
||||
auto modrinthProjectsTask() -> NetJob::Ptr;
|
||||
|
||||
auto flameVersionsTask() -> NetJob::Ptr;
|
||||
auto flameProjectsTask() -> NetJob::Ptr;
|
||||
|
||||
// Helpers
|
||||
void emitReady(Mod*);
|
||||
void emitFail(Mod*);
|
||||
|
||||
auto getHash(Mod*) -> QString;
|
||||
|
||||
private slots:
|
||||
void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
|
||||
void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
|
||||
|
||||
signals:
|
||||
void metadataReady(Mod*);
|
||||
void metadataFailed(Mod*);
|
||||
|
||||
private:
|
||||
QHash<QString, Mod*> m_mods;
|
||||
QDir m_index_dir;
|
||||
ModPlatform::Provider m_provider;
|
||||
|
||||
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
|
||||
NetJob* m_current_task;
|
||||
};
|
@ -40,6 +40,7 @@
|
||||
#include <list>
|
||||
|
||||
#include "Version.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
namespace ModPlatform {
|
||||
class ListModel;
|
||||
@ -74,6 +75,9 @@ class ModAPI {
|
||||
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
|
||||
virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0;
|
||||
|
||||
virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
|
||||
virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
|
||||
|
||||
|
||||
struct VersionSearchArgs {
|
||||
QString addonId;
|
||||
|
@ -54,13 +54,16 @@ struct IndexedVersion {
|
||||
QVariant addonId;
|
||||
QVariant fileId;
|
||||
QString version;
|
||||
QVector<QString> mcVersion;
|
||||
QString version_number = {};
|
||||
QStringList mcVersion;
|
||||
QString downloadUrl;
|
||||
QString date;
|
||||
QString fileName;
|
||||
QVector<QString> loaders = {};
|
||||
QStringList loaders = {};
|
||||
QString hash_type;
|
||||
QString hash;
|
||||
bool is_preferred = true;
|
||||
QString changelog;
|
||||
};
|
||||
|
||||
struct ExtraPackData {
|
||||
@ -76,6 +79,7 @@ struct IndexedPack {
|
||||
QVariant addonId;
|
||||
Provider provider;
|
||||
QString name;
|
||||
QString slug;
|
||||
QString description;
|
||||
QList<ModpackAuthor> authors;
|
||||
QString logoName;
|
||||
|
148
launcher/modplatform/flame/FlameAPI.cpp
Normal file
148
launcher/modplatform/flame/FlameAPI.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "FlameAPI.h"
|
||||
#include "FlameModIndex.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "Json.h"
|
||||
|
||||
#include "net/Upload.h"
|
||||
|
||||
auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network());
|
||||
|
||||
QJsonObject body_obj;
|
||||
QJsonArray fingerprints_arr;
|
||||
for (auto& fp : fingerprints) {
|
||||
fingerprints_arr.append(QString("%1").arg(fp));
|
||||
}
|
||||
|
||||
body_obj["fingerprints"] = fingerprints_arr;
|
||||
|
||||
QJsonDocument body(body_obj);
|
||||
auto body_raw = body.toJson();
|
||||
|
||||
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
|
||||
|
||||
return netJob;
|
||||
}
|
||||
|
||||
auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
|
||||
{
|
||||
QEventLoop lock;
|
||||
QString changelog;
|
||||
|
||||
auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network());
|
||||
auto* response = new QByteArray();
|
||||
netJob->addNetAction(Net::Download::makeByteArray(
|
||||
QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog")
|
||||
.arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))),
|
||||
response));
|
||||
|
||||
QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
netJob->failed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
changelog = Json::ensureString(doc.object(), "data");
|
||||
});
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response, &lock] {
|
||||
delete response;
|
||||
lock.quit();
|
||||
});
|
||||
|
||||
netJob->start();
|
||||
lock.exec();
|
||||
|
||||
return changelog;
|
||||
}
|
||||
|
||||
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
|
||||
{
|
||||
QEventLoop loop;
|
||||
|
||||
auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
|
||||
auto response = new QByteArray();
|
||||
ModPlatform::IndexedVersion ver;
|
||||
|
||||
netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
|
||||
|
||||
QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto obj = Json::requireObject(doc);
|
||||
auto arr = Json::requireArray(obj, "data");
|
||||
|
||||
QJsonObject latest_file_obj;
|
||||
ModPlatform::IndexedVersion ver_tmp;
|
||||
|
||||
for (auto file : arr) {
|
||||
auto file_obj = Json::requireObject(file);
|
||||
auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj);
|
||||
if(file_tmp.date > ver_tmp.date) {
|
||||
ver_tmp = file_tmp;
|
||||
latest_file_obj = file_obj;
|
||||
}
|
||||
}
|
||||
|
||||
ver = FlameMod::loadIndexedPackVersion(latest_file_obj);
|
||||
} catch (Json::JsonException& e) {
|
||||
qCritical() << "Failed to parse response from a version request.";
|
||||
qCritical() << e.what();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] {
|
||||
netJob->deleteLater();
|
||||
delete response;
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
netJob->start();
|
||||
|
||||
loop.exec();
|
||||
|
||||
return ver;
|
||||
}
|
||||
|
||||
auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network());
|
||||
|
||||
QJsonObject body_obj;
|
||||
QJsonArray addons_arr;
|
||||
for (auto& addonId : addonIds) {
|
||||
addons_arr.append(addonId);
|
||||
}
|
||||
|
||||
body_obj["modIds"] = addons_arr;
|
||||
|
||||
QJsonDocument body(body_obj);
|
||||
auto body_raw = body.toJson();
|
||||
|
||||
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
|
||||
QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
|
||||
|
||||
return netJob;
|
||||
}
|
@ -4,6 +4,14 @@
|
||||
#include "modplatform/helpers/NetworkModAPI.h"
|
||||
|
||||
class FlameAPI : public NetworkModAPI {
|
||||
public:
|
||||
auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
|
||||
auto getModFileChangelog(int modId, int fileId) -> QString;
|
||||
|
||||
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
|
||||
|
||||
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
|
||||
|
||||
private:
|
||||
inline auto getSortFieldInt(QString sortString) const -> int
|
||||
{
|
||||
|
179
launcher/modplatform/flame/FlameCheckUpdate.cpp
Normal file
179
launcher/modplatform/flame/FlameCheckUpdate.cpp
Normal file
@ -0,0 +1,179 @@
|
||||
#include "FlameCheckUpdate.h"
|
||||
#include "FlameAPI.h"
|
||||
#include "FlameModIndex.h"
|
||||
|
||||
#include <MurmurHash2.h>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
|
||||
#include "ModDownloadTask.h"
|
||||
|
||||
static FlameAPI api;
|
||||
|
||||
bool FlameCheckUpdate::abort()
|
||||
{
|
||||
m_was_aborted = true;
|
||||
if (m_net_job)
|
||||
return m_net_job->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
|
||||
{
|
||||
ModPlatform::IndexedPack pack;
|
||||
|
||||
QEventLoop loop;
|
||||
|
||||
auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
|
||||
|
||||
auto response = new QByteArray();
|
||||
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
|
||||
auto dl = Net::Download::makeByteArray(url, response);
|
||||
get_project_job->addNetAction(dl);
|
||||
|
||||
QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto doc_obj = Json::requireObject(doc);
|
||||
auto data_obj = Json::requireObject(doc_obj, "data");
|
||||
FlameMod::loadIndexedPack(pack, data_obj);
|
||||
} catch (Json::JsonException& e) {
|
||||
qWarning() << e.cause();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] {
|
||||
get_project_job->deleteLater();
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
get_project_job->start();
|
||||
loop.exec();
|
||||
|
||||
return pack;
|
||||
}
|
||||
|
||||
ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
|
||||
{
|
||||
ModPlatform::IndexedVersion ver;
|
||||
|
||||
QEventLoop loop;
|
||||
|
||||
auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
|
||||
|
||||
auto response = new QByteArray();
|
||||
auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
|
||||
auto dl = Net::Download::makeByteArray(url, response);
|
||||
get_file_info_job->addNetAction(dl);
|
||||
|
||||
QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto doc_obj = Json::requireObject(doc);
|
||||
auto data_obj = Json::requireObject(doc_obj, "data");
|
||||
ver = FlameMod::loadIndexedPackVersion(data_obj);
|
||||
} catch (Json::JsonException& e) {
|
||||
qWarning() << e.cause();
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
|
||||
get_file_info_job->deleteLater();
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
get_file_info_job->start();
|
||||
loop.exec();
|
||||
|
||||
return ver;
|
||||
}
|
||||
|
||||
/* Check for update:
|
||||
* - Get latest version available
|
||||
* - Compare hash of the latest version with the current hash
|
||||
* - If equal, no updates, else, there's updates, so add to the list
|
||||
* */
|
||||
void FlameCheckUpdate::executeTask()
|
||||
{
|
||||
setStatus(tr("Preparing mods for CurseForge..."));
|
||||
|
||||
int i = 0;
|
||||
for (auto* mod : m_mods) {
|
||||
if (!mod->enabled()) {
|
||||
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
|
||||
continue;
|
||||
}
|
||||
|
||||
setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod->name()));
|
||||
setProgress(i++, m_mods.size());
|
||||
|
||||
auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders });
|
||||
|
||||
// Check if we were aborted while getting the latest version
|
||||
if (m_was_aborted) {
|
||||
aborted();
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name()));
|
||||
|
||||
if (!latest_ver.addonId.isValid()) {
|
||||
emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game "
|
||||
"version / mod loader."));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) {
|
||||
auto pack = getProjectInfo(latest_ver);
|
||||
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString());
|
||||
emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
|
||||
// Fake pack with the necessary info to pass to the download task :)
|
||||
ModPlatform::IndexedPack pack;
|
||||
pack.name = mod->name();
|
||||
pack.slug = mod->metadata()->slug;
|
||||
pack.addonId = mod->metadata()->project_id;
|
||||
pack.websiteUrl = mod->homeurl();
|
||||
for (auto& author : mod->authors())
|
||||
pack.authors.append({ author });
|
||||
pack.description = mod->description();
|
||||
pack.provider = ModPlatform::Provider::FLAME;
|
||||
|
||||
auto old_version = mod->version();
|
||||
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
|
||||
auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt());
|
||||
old_version = current_ver.version;
|
||||
}
|
||||
|
||||
auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder);
|
||||
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
|
||||
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
|
||||
ModPlatform::Provider::FLAME, download_task);
|
||||
}
|
||||
}
|
||||
|
||||
emitSucceeded();
|
||||
}
|
25
launcher/modplatform/flame/FlameCheckUpdate.h
Normal file
25
launcher/modplatform/flame/FlameCheckUpdate.h
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "modplatform/CheckUpdateTask.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
class FlameCheckUpdate : public CheckUpdateTask {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
|
||||
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
|
||||
{}
|
||||
|
||||
public slots:
|
||||
bool abort() override;
|
||||
|
||||
protected slots:
|
||||
void executeTask() override;
|
||||
|
||||
private:
|
||||
NetJob* m_net_job = nullptr;
|
||||
|
||||
bool m_was_aborted = false;
|
||||
};
|
@ -7,20 +7,22 @@
|
||||
#include "net/NetJob.h"
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
static FlameAPI api;
|
||||
|
||||
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
|
||||
{
|
||||
pack.addonId = Json::requireInteger(obj, "id");
|
||||
pack.provider = ModPlatform::Provider::FLAME;
|
||||
pack.name = Json::requireString(obj, "name");
|
||||
pack.slug = Json::requireString(obj, "slug");
|
||||
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
|
||||
pack.description = Json::ensureString(obj, "summary", "");
|
||||
|
||||
QJsonObject logo = Json::requireObject(obj, "logo");
|
||||
pack.logoName = Json::requireString(logo, "title");
|
||||
pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
|
||||
QJsonObject logo = Json::ensureObject(obj, "logo");
|
||||
pack.logoName = Json::ensureString(logo, "title");
|
||||
pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
|
||||
|
||||
auto authors = Json::requireArray(obj, "authors");
|
||||
auto authors = Json::ensureArray(obj, "authors");
|
||||
for (auto authorIter : authors) {
|
||||
auto author = Json::requireObject(authorIter);
|
||||
ModPlatform::ModpackAuthor packAuthor;
|
||||
@ -91,7 +93,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
|
||||
pack.versionsLoaded = true;
|
||||
}
|
||||
|
||||
auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion
|
||||
auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion
|
||||
{
|
||||
auto versionArray = Json::requireArray(obj, "gameVersions");
|
||||
if (versionArray.isEmpty()) {
|
||||
@ -110,7 +112,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV
|
||||
file.fileId = Json::requireInteger(obj, "id");
|
||||
file.date = Json::requireString(obj, "fileDate");
|
||||
file.version = Json::requireString(obj, "displayName");
|
||||
file.downloadUrl = Json::requireString(obj, "downloadUrl");
|
||||
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
|
||||
file.fileName = Json::requireString(obj, "fileName");
|
||||
|
||||
auto hash_list = Json::ensureArray(obj, "hashes");
|
||||
@ -124,5 +126,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(load_changelog)
|
||||
file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
|
||||
|
||||
return file;
|
||||
}
|
||||
|
@ -17,6 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
|
||||
QJsonArray& arr,
|
||||
const shared_qobject_ptr<QNetworkAccessManager>& network,
|
||||
BaseInstance* inst);
|
||||
auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
|
||||
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
|
||||
|
||||
} // namespace FlameMod
|
||||
|
@ -33,18 +33,14 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
|
||||
|
||||
void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack)
|
||||
{
|
||||
auto id_str = pack.addonId.toString();
|
||||
auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network());
|
||||
auto searchUrl = getModInfoURL(id_str);
|
||||
|
||||
auto response = new QByteArray();
|
||||
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
|
||||
auto job = getProject(pack.addonId.toString(), response);
|
||||
|
||||
QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] {
|
||||
QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] {
|
||||
QJsonParseError parse_error{};
|
||||
auto doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset
|
||||
qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
return;
|
||||
@ -53,7 +49,7 @@ void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pac
|
||||
caller->infoRequestFinished(doc, pack);
|
||||
});
|
||||
|
||||
netJob->start();
|
||||
job->start();
|
||||
}
|
||||
|
||||
void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const
|
||||
@ -83,3 +79,18 @@ void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) co
|
||||
|
||||
netJob->start();
|
||||
}
|
||||
|
||||
auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob*
|
||||
{
|
||||
auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
|
||||
auto searchUrl = getModInfoURL(addonId);
|
||||
|
||||
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response, netJob] {
|
||||
netJob->deleteLater();
|
||||
delete response;
|
||||
});
|
||||
|
||||
return netJob;
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ class NetworkModAPI : public ModAPI {
|
||||
void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override;
|
||||
void getVersions(CallerType* caller, VersionSearchArgs&& args) const override;
|
||||
|
||||
auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
|
||||
|
||||
protected:
|
||||
virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
|
||||
virtual auto getModInfoURL(QString& id) const -> QString = 0;
|
||||
|
108
launcher/modplatform/modrinth/ModrinthAPI.cpp
Normal file
108
launcher/modplatform/modrinth/ModrinthAPI.cpp
Normal file
@ -0,0 +1,108 @@
|
||||
#include "ModrinthAPI.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "Json.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
|
||||
|
||||
netJob->addNetAction(Net::Download::makeByteArray(
|
||||
QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
|
||||
|
||||
return netJob;
|
||||
}
|
||||
|
||||
auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
|
||||
|
||||
QJsonObject body_obj;
|
||||
|
||||
Json::writeStringList(body_obj, "hashes", hashes);
|
||||
Json::writeString(body_obj, "algorithm", hash_format);
|
||||
|
||||
QJsonDocument body(body_obj);
|
||||
auto body_raw = body.toJson();
|
||||
|
||||
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
|
||||
|
||||
return netJob;
|
||||
}
|
||||
|
||||
auto ModrinthAPI::latestVersion(QString hash,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
|
||||
|
||||
QJsonObject body_obj;
|
||||
|
||||
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
|
||||
|
||||
QStringList game_versions;
|
||||
for (auto& ver : mcVersions) {
|
||||
game_versions.append(ver.toString());
|
||||
}
|
||||
Json::writeStringList(body_obj, "game_versions", game_versions);
|
||||
|
||||
QJsonDocument body(body_obj);
|
||||
auto body_raw = body.toJson();
|
||||
|
||||
netJob->addNetAction(Net::Upload::makeByteArray(
|
||||
QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
|
||||
|
||||
return netJob;
|
||||
}
|
||||
|
||||
auto ModrinthAPI::latestVersions(const QStringList& hashes,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
|
||||
|
||||
QJsonObject body_obj;
|
||||
|
||||
Json::writeStringList(body_obj, "hashes", hashes);
|
||||
Json::writeString(body_obj, "algorithm", hash_format);
|
||||
|
||||
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
|
||||
|
||||
QStringList game_versions;
|
||||
for (auto& ver : mcVersions) {
|
||||
game_versions.append(ver.toString());
|
||||
}
|
||||
Json::writeStringList(body_obj, "game_versions", game_versions);
|
||||
|
||||
QJsonDocument body(body_obj);
|
||||
auto body_raw = body.toJson();
|
||||
|
||||
netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
|
||||
|
||||
return netJob;
|
||||
}
|
||||
|
||||
auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
|
||||
{
|
||||
auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
|
||||
auto searchUrl = getMultipleModInfoURL(addonIds);
|
||||
|
||||
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
|
||||
|
||||
QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
|
||||
|
||||
return netJob;
|
||||
}
|
@ -26,6 +26,29 @@
|
||||
#include <QDebug>
|
||||
|
||||
class ModrinthAPI : public NetworkModAPI {
|
||||
public:
|
||||
auto currentVersion(QString hash,
|
||||
QString hash_format,
|
||||
QByteArray* response) -> NetJob::Ptr;
|
||||
|
||||
auto currentVersions(const QStringList& hashes,
|
||||
QString hash_format,
|
||||
QByteArray* response) -> NetJob::Ptr;
|
||||
|
||||
auto latestVersion(QString hash,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
QByteArray* response) -> NetJob::Ptr;
|
||||
|
||||
auto latestVersions(const QStringList& hashes,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
QByteArray* response) -> NetJob::Ptr;
|
||||
|
||||
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
|
||||
|
||||
public:
|
||||
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
|
||||
|
||||
@ -81,6 +104,11 @@ class ModrinthAPI : public NetworkModAPI {
|
||||
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
|
||||
};
|
||||
|
||||
inline auto getMultipleModInfoURL(QStringList ids) const -> QString
|
||||
{
|
||||
return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
|
||||
};
|
||||
|
||||
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
|
||||
{
|
||||
return QString(BuildConfig.MODRINTH_PROD_URL +
|
||||
|
174
launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
Normal file
174
launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
Normal file
@ -0,0 +1,174 @@
|
||||
#include "ModrinthCheckUpdate.h"
|
||||
#include "ModrinthAPI.h"
|
||||
#include "ModrinthPackIndex.h"
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
|
||||
#include "ModDownloadTask.h"
|
||||
|
||||
static ModrinthAPI api;
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
bool ModrinthCheckUpdate::abort()
|
||||
{
|
||||
if (m_net_job)
|
||||
return m_net_job->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Check for update:
|
||||
* - Get latest version available
|
||||
* - Compare hash of the latest version with the current hash
|
||||
* - If equal, no updates, else, there's updates, so add to the list
|
||||
* */
|
||||
void ModrinthCheckUpdate::executeTask()
|
||||
{
|
||||
setStatus(tr("Preparing mods for Modrinth..."));
|
||||
setProgress(0, 3);
|
||||
|
||||
QHash<QString, Mod*> mappings;
|
||||
|
||||
// Create all hashes
|
||||
QStringList hashes;
|
||||
auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
||||
for (auto* mod : m_mods) {
|
||||
if (!mod->enabled()) {
|
||||
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto hash = mod->metadata()->hash;
|
||||
|
||||
// Sadly the API can only handle one hash type per call, se we
|
||||
// need to generate a new hash if the current one is innadequate
|
||||
// (though it will rarely happen, if at all)
|
||||
if (mod->metadata()->hash_format != best_hash_type) {
|
||||
QByteArray jar_data;
|
||||
|
||||
try {
|
||||
jar_data = FS::read(mod->fileinfo().absoluteFilePath());
|
||||
} catch (FS::FileSystemException& e) {
|
||||
qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
|
||||
qCritical() << QString("Reason: ") << e.cause();
|
||||
|
||||
failed(e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex());
|
||||
}
|
||||
|
||||
hashes.append(hash);
|
||||
mappings.insert(hash, mod);
|
||||
}
|
||||
|
||||
auto* response = new QByteArray();
|
||||
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
|
||||
|
||||
QEventLoop lock;
|
||||
|
||||
connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
failed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(tr("Parsing the API response from Modrinth..."));
|
||||
setProgress(2, 3);
|
||||
|
||||
try {
|
||||
for (auto hash : mappings.keys()) {
|
||||
auto project_obj = doc[hash].toObject();
|
||||
|
||||
// If the returned project is empty, but we have Modrinth metadata,
|
||||
// it means this specific version is not available
|
||||
if (project_obj.isEmpty()) {
|
||||
qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response.";
|
||||
qDebug() << "Hash: " << hash;
|
||||
|
||||
emit checkFailed(
|
||||
mappings.find(hash).value(),
|
||||
tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
|
||||
// so we may want to filter it
|
||||
QString loader_filter;
|
||||
static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
|
||||
for (auto flag : flags) {
|
||||
if (m_loaders.testFlag(flag)) {
|
||||
loader_filter = api.getModLoaderString(flag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
|
||||
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
|
||||
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
|
||||
// Such is the pain of having arbitrary files for a given version .-.
|
||||
|
||||
auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter);
|
||||
if (project_ver.downloadUrl.isEmpty()) {
|
||||
qCritical() << "Modrinth mod without download url!";
|
||||
qCritical() << project_ver.fileName;
|
||||
|
||||
emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL"));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
auto mod_iter = mappings.find(hash);
|
||||
if (mod_iter == mappings.end()) {
|
||||
qCritical() << "Failed to remap mod from Modrinth!";
|
||||
continue;
|
||||
}
|
||||
auto mod = *mod_iter;
|
||||
|
||||
auto key = project_ver.hash;
|
||||
if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
|
||||
if (mod->version() == project_ver.version_number)
|
||||
continue;
|
||||
|
||||
// Fake pack with the necessary info to pass to the download task :)
|
||||
ModPlatform::IndexedPack pack;
|
||||
pack.name = mod->name();
|
||||
pack.slug = mod->metadata()->slug;
|
||||
pack.addonId = mod->metadata()->project_id;
|
||||
pack.websiteUrl = mod->homeurl();
|
||||
for (auto& author : mod->authors())
|
||||
pack.authors.append({ author });
|
||||
pack.description = mod->description();
|
||||
pack.provider = ModPlatform::Provider::MODRINTH;
|
||||
|
||||
auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder);
|
||||
|
||||
m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
|
||||
ModPlatform::Provider::MODRINTH, download_task);
|
||||
}
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
failed(e.cause() + " : " + e.what());
|
||||
}
|
||||
});
|
||||
|
||||
connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
|
||||
|
||||
setStatus(tr("Waiting for the API response from Modrinth..."));
|
||||
setProgress(1, 3);
|
||||
|
||||
m_net_job = job.get();
|
||||
job->start();
|
||||
|
||||
lock.exec();
|
||||
|
||||
emitSucceeded();
|
||||
}
|
23
launcher/modplatform/modrinth/ModrinthCheckUpdate.h
Normal file
23
launcher/modplatform/modrinth/ModrinthCheckUpdate.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "modplatform/CheckUpdateTask.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
class ModrinthCheckUpdate : public CheckUpdateTask {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
|
||||
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
|
||||
{}
|
||||
|
||||
public slots:
|
||||
bool abort() override;
|
||||
|
||||
protected slots:
|
||||
void executeTask() override;
|
||||
|
||||
private:
|
||||
NetJob* m_net_job = nullptr;
|
||||
};
|
@ -29,13 +29,16 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
|
||||
{
|
||||
pack.addonId = Json::requireString(obj, "project_id");
|
||||
pack.addonId = Json::ensureString(obj, "project_id");
|
||||
if (pack.addonId.toString().isEmpty())
|
||||
pack.addonId = Json::requireString(obj, "id");
|
||||
|
||||
pack.provider = ModPlatform::Provider::MODRINTH;
|
||||
pack.name = Json::requireString(obj, "title");
|
||||
|
||||
QString slug = Json::ensureString(obj, "slug", "");
|
||||
if (!slug.isEmpty())
|
||||
pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", "");
|
||||
pack.slug = Json::ensureString(obj, "slug", "");
|
||||
if (!pack.slug.isEmpty())
|
||||
pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
|
||||
else
|
||||
pack.websiteUrl = "";
|
||||
|
||||
@ -45,7 +48,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
|
||||
pack.logoName = pack.addonId.toString();
|
||||
|
||||
ModPlatform::ModpackAuthor modAuthor;
|
||||
modAuthor.name = Json::requireString(obj, "author");
|
||||
modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)"));
|
||||
modAuthor.url = api.getAuthorURL(modAuthor.name);
|
||||
pack.authors.append(modAuthor);
|
||||
|
||||
@ -111,7 +114,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
|
||||
pack.versionsLoaded = true;
|
||||
}
|
||||
|
||||
auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion
|
||||
auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion
|
||||
{
|
||||
ModPlatform::IndexedVersion file;
|
||||
|
||||
@ -130,6 +133,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
|
||||
file.loaders.append(loader.toString());
|
||||
}
|
||||
file.version = Json::requireString(obj, "name");
|
||||
file.version_number = Json::requireString(obj, "version_number");
|
||||
file.changelog = Json::requireString(obj, "changelog");
|
||||
|
||||
auto files = Json::requireArray(obj, "files");
|
||||
int i = 0;
|
||||
@ -142,6 +147,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
|
||||
auto parent = files[i].toObject();
|
||||
auto fileName = Json::requireString(parent, "filename");
|
||||
|
||||
if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) {
|
||||
file.is_preferred = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Grab the primary file, if available
|
||||
if (Json::requireBoolean(parent, "primary"))
|
||||
break;
|
||||
@ -153,13 +163,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
|
||||
if (parent.contains("url")) {
|
||||
file.downloadUrl = Json::requireString(parent, "url");
|
||||
file.fileName = Json::requireString(parent, "filename");
|
||||
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
|
||||
auto hash_list = Json::requireObject(parent, "hashes");
|
||||
auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
|
||||
for (auto& hash_type : hash_types) {
|
||||
if (hash_list.contains(hash_type)) {
|
||||
file.hash = Json::requireString(hash_list, hash_type);
|
||||
file.hash_type = hash_type;
|
||||
break;
|
||||
|
||||
if (hash_list.contains(preferred_hash_type)) {
|
||||
file.hash = Json::requireString(hash_list, preferred_hash_type);
|
||||
file.hash_type = preferred_hash_type;
|
||||
} else {
|
||||
auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
|
||||
for (auto& hash_type : hash_types) {
|
||||
if (hash_list.contains(hash_type)) {
|
||||
file.hash = Json::requireString(hash_list, hash_type);
|
||||
file.hash_type = hash_type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
|
||||
QJsonArray& arr,
|
||||
const shared_qobject_ptr<QNetworkAccessManager>& network,
|
||||
BaseInstance* inst);
|
||||
auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
|
||||
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
|
||||
|
||||
} // namespace Modrinth
|
||||
|
@ -55,11 +55,11 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin
|
||||
}
|
||||
|
||||
// Helpers
|
||||
static inline auto indexFileName(QString const& mod_name) -> QString
|
||||
static inline auto indexFileName(QString const& mod_slug) -> QString
|
||||
{
|
||||
if(mod_name.endsWith(".pw.toml"))
|
||||
return mod_name;
|
||||
return QString("%1.pw.toml").arg(mod_name);
|
||||
if(mod_slug.endsWith(".pw.toml"))
|
||||
return mod_slug;
|
||||
return QString("%1.pw.toml").arg(mod_slug);
|
||||
}
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
@ -95,6 +95,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
|
||||
{
|
||||
Mod mod;
|
||||
|
||||
mod.slug = mod_pack.slug;
|
||||
mod.name = mod_pack.name;
|
||||
mod.filename = mod_version.fileName;
|
||||
|
||||
@ -116,12 +117,10 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
|
||||
return mod;
|
||||
}
|
||||
|
||||
auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod
|
||||
auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod
|
||||
{
|
||||
auto mod_name = internal_mod.name();
|
||||
|
||||
// Try getting metadata if it exists
|
||||
Mod mod { getIndexForMod(index_dir, mod_name) };
|
||||
Mod mod { getIndexForMod(index_dir, slug) };
|
||||
if(mod.isValid())
|
||||
return mod;
|
||||
|
||||
@ -139,11 +138,14 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
|
||||
|
||||
// Ensure the corresponding mod's info exists, and create it if not
|
||||
|
||||
auto normalized_fname = indexFileName(mod.name);
|
||||
auto normalized_fname = indexFileName(mod.slug);
|
||||
auto real_fname = getRealIndexName(index_dir, normalized_fname);
|
||||
|
||||
QFile index_file(index_dir.absoluteFilePath(real_fname));
|
||||
|
||||
if (real_fname != normalized_fname)
|
||||
index_file.rename(normalized_fname);
|
||||
|
||||
// There's already data on there!
|
||||
// TODO: We should do more stuff here, as the user is likely trying to
|
||||
// override a file. In this case, check versions and ask the user what
|
||||
@ -184,33 +186,46 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
|
||||
}
|
||||
}
|
||||
|
||||
index_file.flush();
|
||||
index_file.close();
|
||||
}
|
||||
|
||||
void V1::deleteModIndex(QDir& index_dir, QString& mod_name)
|
||||
void V1::deleteModIndex(QDir& index_dir, QString& mod_slug)
|
||||
{
|
||||
auto normalized_fname = indexFileName(mod_name);
|
||||
auto normalized_fname = indexFileName(mod_slug);
|
||||
auto real_fname = getRealIndexName(index_dir, normalized_fname);
|
||||
if (real_fname.isEmpty())
|
||||
return;
|
||||
|
||||
QFile index_file(index_dir.absoluteFilePath(real_fname));
|
||||
|
||||
if(!index_file.exists()){
|
||||
qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name);
|
||||
if (!index_file.exists()) {
|
||||
qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!index_file.remove()){
|
||||
qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name);
|
||||
if (!index_file.remove()) {
|
||||
qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug);
|
||||
}
|
||||
}
|
||||
|
||||
auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
|
||||
void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id)
|
||||
{
|
||||
for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
|
||||
auto mod = getIndexForMod(index_dir, file_name);
|
||||
|
||||
if (mod.mod_id() == mod_id) {
|
||||
deleteModIndex(index_dir, mod.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
|
||||
{
|
||||
Mod mod;
|
||||
|
||||
auto normalized_fname = indexFileName(index_file_name);
|
||||
auto normalized_fname = indexFileName(slug);
|
||||
auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
|
||||
if (real_fname.isEmpty())
|
||||
return {};
|
||||
@ -218,7 +233,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
|
||||
QFile index_file(index_dir.absoluteFilePath(real_fname));
|
||||
|
||||
if (!index_file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name);
|
||||
qWarning() << QString("Failed to open mod metadata for %1").arg(slug);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -232,11 +247,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
|
||||
index_file.close();
|
||||
|
||||
if (!table) {
|
||||
qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name));
|
||||
qWarning() << QString("Could not open file %1!").arg(normalized_fname);
|
||||
qWarning() << "Reason: " << QString(errbuf);
|
||||
return {};
|
||||
}
|
||||
|
||||
mod.slug = slug;
|
||||
|
||||
{ // Basic info
|
||||
mod.name = stringEntry(table, "name");
|
||||
mod.filename = stringEntry(table, "filename");
|
||||
@ -286,4 +303,16 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
|
||||
return mod;
|
||||
}
|
||||
|
||||
} // namespace Packwiz
|
||||
auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod
|
||||
{
|
||||
for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
|
||||
auto mod = getIndexForMod(index_dir, file_name);
|
||||
|
||||
if (mod.mod_id() == mod_id)
|
||||
return mod;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Packwiz
|
||||
|
@ -40,6 +40,7 @@ auto intEntry(toml_table_t* parent, const char* entry_name) -> int;
|
||||
class V1 {
|
||||
public:
|
||||
struct Mod {
|
||||
QString slug {};
|
||||
QString name {};
|
||||
QString filename {};
|
||||
// FIXME: make side an enum
|
||||
@ -58,7 +59,7 @@ class V1 {
|
||||
|
||||
public:
|
||||
// This is a totally heuristic, but should work for now.
|
||||
auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); }
|
||||
auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); }
|
||||
|
||||
// Different providers can use different names for the same thing
|
||||
// Modrinth-specific
|
||||
@ -71,9 +72,9 @@ class V1 {
|
||||
* */
|
||||
static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod;
|
||||
/* Generates the object representing the information in a mod.pw.toml file via
|
||||
* its common representation in the launcher.
|
||||
* its common representation in the launcher, plus a necessary slug.
|
||||
* */
|
||||
static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod;
|
||||
static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod;
|
||||
|
||||
/* Updates the mod index for the provided mod.
|
||||
* This creates a new index if one does not exist already
|
||||
@ -81,13 +82,21 @@ class V1 {
|
||||
* */
|
||||
static void updateModIndex(QDir& index_dir, Mod& mod);
|
||||
|
||||
/* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */
|
||||
static void deleteModIndex(QDir& index_dir, QString& mod_name);
|
||||
/* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */
|
||||
static void deleteModIndex(QDir& index_dir, QString& mod_slug);
|
||||
|
||||
/* Gets the metadata for a mod with a particular name.
|
||||
/* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */
|
||||
static void deleteModIndex(QDir& index_dir, QVariant& mod_id);
|
||||
|
||||
/* Gets the metadata for a mod with a particular file name.
|
||||
* If the mod doesn't have a metadata, it simply returns an empty Mod object.
|
||||
* */
|
||||
static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod;
|
||||
static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod;
|
||||
|
||||
/* Gets the metadata for a mod with a particular id.
|
||||
* If the mod doesn't have a metadata, it simply returns an empty Mod object.
|
||||
* */
|
||||
static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod;
|
||||
};
|
||||
|
||||
} // namespace Packwiz
|
||||
|
@ -32,10 +32,11 @@ class PackwizTest : public QObject {
|
||||
QString source = QFINDTESTDATA("testdata");
|
||||
|
||||
QDir index_dir(source);
|
||||
QString name_mod("borderless-mining.pw.toml");
|
||||
QVERIFY(index_dir.entryList().contains(name_mod));
|
||||
QString slug_mod("borderless-mining");
|
||||
QString file_name = slug_mod + ".pw.toml";
|
||||
QVERIFY(index_dir.entryList().contains(file_name));
|
||||
|
||||
auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod);
|
||||
auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod);
|
||||
|
||||
QVERIFY(metadata.isValid());
|
||||
|
||||
|
@ -43,6 +43,16 @@
|
||||
|
||||
namespace Net {
|
||||
|
||||
bool Upload::abort()
|
||||
{
|
||||
if (m_reply) {
|
||||
m_reply->abort();
|
||||
} else {
|
||||
m_state = State::AbortedByUser;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
|
||||
setProgress(bytesReceived, bytesTotal);
|
||||
}
|
||||
|
@ -46,6 +46,8 @@ namespace Net {
|
||||
|
||||
public:
|
||||
static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
|
||||
auto abort() -> bool override;
|
||||
auto canAbort() const -> bool override { return true; };
|
||||
|
||||
protected slots:
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
|
||||
|
48
launcher/tasks/MultipleOptionsTask.cpp
Normal file
48
launcher/tasks/MultipleOptionsTask.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#include "MultipleOptionsTask.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : SequentialTask(parent, task_name) {}
|
||||
|
||||
void MultipleOptionsTask::startNext()
|
||||
{
|
||||
Task* previous = nullptr;
|
||||
if (m_currentIndex != -1) {
|
||||
previous = m_queue[m_currentIndex].get();
|
||||
disconnect(previous, 0, this, 0);
|
||||
}
|
||||
|
||||
m_currentIndex++;
|
||||
if ((previous && previous->wasSuccessful())) {
|
||||
emitSucceeded();
|
||||
return;
|
||||
}
|
||||
|
||||
Task::Ptr next = m_queue[m_currentIndex];
|
||||
|
||||
connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed);
|
||||
connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext);
|
||||
|
||||
connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus);
|
||||
connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus);
|
||||
|
||||
connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress);
|
||||
|
||||
qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size());
|
||||
setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
|
||||
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
|
||||
|
||||
next->start();
|
||||
}
|
||||
|
||||
void MultipleOptionsTask::subTaskFailed(QString const& reason)
|
||||
{
|
||||
qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason);
|
||||
if(m_currentIndex < m_queue.size() - 1) {
|
||||
startNext();
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << QString("All attempts have failed!");
|
||||
emitFailed();
|
||||
}
|
19
launcher/tasks/MultipleOptionsTask.h
Normal file
19
launcher/tasks/MultipleOptionsTask.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "SequentialTask.h"
|
||||
|
||||
/* This task type will attempt to do run each of it's subtasks in sequence,
|
||||
* until one of them succeeds. When that happens, the remaining tasks will not run.
|
||||
* */
|
||||
class MultipleOptionsTask : public SequentialTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = "");
|
||||
virtual ~MultipleOptionsTask() = default;
|
||||
|
||||
private
|
||||
slots:
|
||||
void startNext() override;
|
||||
void subTaskFailed(const QString &msg) override;
|
||||
};
|
@ -1,5 +1,7 @@
|
||||
#include "SequentialTask.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {}
|
||||
|
||||
SequentialTask::~SequentialTask()
|
||||
@ -39,14 +41,15 @@ bool SequentialTask::abort()
|
||||
emit aborted();
|
||||
emit finished();
|
||||
}
|
||||
m_queue.clear();
|
||||
|
||||
m_aborted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool succeeded = m_queue[m_currentIndex]->abort();
|
||||
m_queue.clear();
|
||||
m_aborted = succeeded;
|
||||
|
||||
if(succeeded)
|
||||
if (succeeded)
|
||||
emitAborted();
|
||||
|
||||
return succeeded;
|
||||
@ -54,10 +57,14 @@ bool SequentialTask::abort()
|
||||
|
||||
void SequentialTask::startNext()
|
||||
{
|
||||
if (m_currentIndex != -1) {
|
||||
Task::Ptr previous = m_queue[m_currentIndex];
|
||||
if (m_aborted)
|
||||
return;
|
||||
|
||||
if (m_currentIndex != -1 && m_currentIndex < m_queue.size()) {
|
||||
Task::Ptr previous = m_queue.at(m_currentIndex);
|
||||
disconnect(previous.get(), 0, this, 0);
|
||||
}
|
||||
|
||||
m_currentIndex++;
|
||||
if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) {
|
||||
emitSucceeded();
|
||||
@ -76,6 +83,8 @@ void SequentialTask::startNext()
|
||||
setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
|
||||
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
|
||||
|
||||
setProgress(m_currentIndex + 1, m_queue.count());
|
||||
|
||||
next->start();
|
||||
}
|
||||
|
||||
@ -93,7 +102,6 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total)
|
||||
setProgress(0, 100);
|
||||
return;
|
||||
}
|
||||
setProgress(m_currentIndex + 1, m_queue.count());
|
||||
|
||||
m_stepProgress = current;
|
||||
m_stepTotalProgress = total;
|
||||
|
@ -20,17 +20,17 @@ public:
|
||||
|
||||
void addTask(Task::Ptr task);
|
||||
|
||||
protected slots:
|
||||
void executeTask() override;
|
||||
public slots:
|
||||
bool abort() override;
|
||||
|
||||
private
|
||||
protected
|
||||
slots:
|
||||
void startNext();
|
||||
void subTaskFailed(const QString &msg);
|
||||
void subTaskStatus(const QString &msg);
|
||||
void subTaskProgress(qint64 current, qint64 total);
|
||||
void executeTask() override;
|
||||
|
||||
virtual void startNext();
|
||||
virtual void subTaskFailed(const QString &msg);
|
||||
virtual void subTaskStatus(const QString &msg);
|
||||
virtual void subTaskProgress(qint64 current, qint64 total);
|
||||
|
||||
protected:
|
||||
void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); };
|
||||
@ -44,4 +44,6 @@ protected:
|
||||
|
||||
qint64 m_stepProgress = 0;
|
||||
qint64 m_stepTotalProgress = 100;
|
||||
|
||||
bool m_aborted = false;
|
||||
};
|
||||
|
96
launcher/ui/dialogs/ChooseProviderDialog.cpp
Normal file
96
launcher/ui/dialogs/ChooseProviderDialog.cpp
Normal file
@ -0,0 +1,96 @@
|
||||
#include "ChooseProviderDialog.h"
|
||||
#include "ui_ChooseProviderDialog.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
|
||||
#include "modplatform/ModIndex.h"
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping)
|
||||
: QDialog(parent), ui(new Ui::ChooseProviderDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
addProviders();
|
||||
m_providers.button(0)->click();
|
||||
|
||||
connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne);
|
||||
connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll);
|
||||
|
||||
connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne);
|
||||
connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll);
|
||||
|
||||
if (single_choice) {
|
||||
ui->providersLayout->removeWidget(ui->skipAllButton);
|
||||
ui->providersLayout->removeWidget(ui->confirmAllButton);
|
||||
}
|
||||
|
||||
if (!allow_skipping) {
|
||||
ui->providersLayout->removeWidget(ui->skipOneButton);
|
||||
ui->providersLayout->removeWidget(ui->skipAllButton);
|
||||
}
|
||||
}
|
||||
|
||||
ChooseProviderDialog::~ChooseProviderDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void ChooseProviderDialog::setDescription(QString desc)
|
||||
{
|
||||
ui->explanationLabel->setText(desc);
|
||||
}
|
||||
|
||||
void ChooseProviderDialog::skipOne()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
void ChooseProviderDialog::skipAll()
|
||||
{
|
||||
m_response.skip_all = true;
|
||||
reject();
|
||||
}
|
||||
|
||||
void ChooseProviderDialog::confirmOne()
|
||||
{
|
||||
m_response.chosen = getSelectedProvider();
|
||||
m_response.try_others = ui->tryOthersCheckbox->isChecked();
|
||||
accept();
|
||||
}
|
||||
void ChooseProviderDialog::confirmAll()
|
||||
{
|
||||
m_response.chosen = getSelectedProvider();
|
||||
m_response.confirm_all = true;
|
||||
m_response.try_others = ui->tryOthersCheckbox->isChecked();
|
||||
accept();
|
||||
}
|
||||
|
||||
auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider
|
||||
{
|
||||
return ModPlatform::Provider(m_providers.checkedId());
|
||||
}
|
||||
|
||||
void ChooseProviderDialog::addProviders()
|
||||
{
|
||||
int btn_index = 0;
|
||||
QRadioButton* btn;
|
||||
|
||||
for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) {
|
||||
btn = new QRadioButton(ProviderCaps.readableName(provider), this);
|
||||
m_providers.addButton(btn, btn_index++);
|
||||
ui->providersLayout->addWidget(btn);
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseProviderDialog::disableInput()
|
||||
{
|
||||
for (auto& btn : m_providers.buttons())
|
||||
btn->setEnabled(false);
|
||||
|
||||
ui->skipOneButton->setEnabled(false);
|
||||
ui->skipAllButton->setEnabled(false);
|
||||
ui->confirmOneButton->setEnabled(false);
|
||||
ui->confirmAllButton->setEnabled(false);
|
||||
}
|
56
launcher/ui/dialogs/ChooseProviderDialog.h
Normal file
56
launcher/ui/dialogs/ChooseProviderDialog.h
Normal file
@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QDialog>
|
||||
|
||||
namespace Ui {
|
||||
class ChooseProviderDialog;
|
||||
}
|
||||
|
||||
namespace ModPlatform {
|
||||
enum class Provider;
|
||||
}
|
||||
|
||||
class Mod;
|
||||
class NetJob;
|
||||
class ModUpdateDialog;
|
||||
|
||||
class ChooseProviderDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
struct Response {
|
||||
bool skip_all = false;
|
||||
bool confirm_all = false;
|
||||
|
||||
bool try_others = false;
|
||||
|
||||
ModPlatform::Provider chosen;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true);
|
||||
~ChooseProviderDialog();
|
||||
|
||||
auto getResponse() const -> Response { return m_response; }
|
||||
|
||||
void setDescription(QString desc);
|
||||
|
||||
private slots:
|
||||
void skipOne();
|
||||
void skipAll();
|
||||
void confirmOne();
|
||||
void confirmAll();
|
||||
|
||||
private:
|
||||
void addProviders();
|
||||
void disableInput();
|
||||
|
||||
auto getSelectedProvider() const -> ModPlatform::Provider;
|
||||
|
||||
private:
|
||||
Ui::ChooseProviderDialog* ui;
|
||||
|
||||
QButtonGroup m_providers;
|
||||
|
||||
Response m_response;
|
||||
};
|
89
launcher/ui/dialogs/ChooseProviderDialog.ui
Normal file
89
launcher/ui/dialogs/ChooseProviderDialog.ui
Normal file
@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ChooseProviderDialog</class>
|
||||
<widget class="QDialog" name="ChooseProviderDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>453</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Choose a mod provider</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="explanationLabel">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignJustify|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<layout class="QFormLayout" name="providersLayout">
|
||||
<property name="labelAlignment">
|
||||
<set>Qt::AlignHCenter|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="formAlignment">
|
||||
<set>Qt::AlignHCenter|Qt::AlignTop</set>
|
||||
</property>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="buttonsLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="skipOneButton">
|
||||
<property name="text">
|
||||
<string>Skip this mod</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="skipAllButton">
|
||||
<property name="text">
|
||||
<string>Skip all</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="confirmAllButton">
|
||||
<property name="text">
|
||||
<string>Confirm for all</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="confirmOneButton">
|
||||
<property name="text">
|
||||
<string>Confirm</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="tryOthersCheckbox">
|
||||
<property name="text">
|
||||
<string>Try to automatically use other providers if the chosen one fails</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
408
launcher/ui/dialogs/ModUpdateDialog.cpp
Normal file
408
launcher/ui/dialogs/ModUpdateDialog.cpp
Normal file
@ -0,0 +1,408 @@
|
||||
#include "ModUpdateDialog.h"
|
||||
#include "ChooseProviderDialog.h"
|
||||
#include "CustomMessageBox.h"
|
||||
#include "ProgressDialog.h"
|
||||
#include "ScrollMessageBox.h"
|
||||
#include "ui_ReviewMessageBox.h"
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
|
||||
#include "tasks/ConcurrentTask.h"
|
||||
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "minecraft/PackProfile.h"
|
||||
|
||||
#include "modplatform/EnsureMetadataTask.h"
|
||||
#include "modplatform/flame/FlameCheckUpdate.h"
|
||||
#include "modplatform/modrinth/ModrinthCheckUpdate.h"
|
||||
|
||||
#include <HoeDown.h>
|
||||
#include <QTextBrowser>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
static std::list<Version> mcVersions(BaseInstance* inst)
|
||||
{
|
||||
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
|
||||
}
|
||||
|
||||
static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
|
||||
{
|
||||
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
|
||||
}
|
||||
|
||||
ModUpdateDialog::ModUpdateDialog(QWidget* parent,
|
||||
BaseInstance* instance,
|
||||
const std::shared_ptr<ModFolderModel> mods,
|
||||
QList<Mod::Ptr>& search_for)
|
||||
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
|
||||
, m_parent(parent)
|
||||
, m_mod_model(mods)
|
||||
, m_candidates(search_for)
|
||||
, m_second_try_metadata(new ConcurrentTask())
|
||||
, m_instance(instance)
|
||||
{
|
||||
ReviewMessageBox::setGeometry(0, 0, 800, 600);
|
||||
|
||||
ui->explainLabel->setText(tr("You're about to update the following mods:"));
|
||||
ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!"));
|
||||
}
|
||||
|
||||
void ModUpdateDialog::checkCandidates()
|
||||
{
|
||||
// Ensure mods have valid metadata
|
||||
auto went_well = ensureMetadata();
|
||||
if (!went_well) {
|
||||
m_aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Report failed metadata generation
|
||||
if (!m_failed_metadata.empty()) {
|
||||
QString text;
|
||||
for (const auto& failed : m_failed_metadata) {
|
||||
const auto& mod = std::get<0>(failed);
|
||||
const auto& reason = std::get<1>(failed);
|
||||
text += tr("Mod name: %1<br>File name: %2<br>Reason: %3<br><br>").arg(mod->name(), mod->fileinfo().fileName(), reason);
|
||||
}
|
||||
|
||||
ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"),
|
||||
tr("Could not generate metadata for the following mods:<br>"
|
||||
"Do you wish to proceed without those mods?"),
|
||||
text);
|
||||
message_dialog.setModal(true);
|
||||
if (message_dialog.exec() == QDialog::Rejected) {
|
||||
m_aborted = true;
|
||||
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto versions = mcVersions(m_instance);
|
||||
auto loaders = mcLoaders(m_instance);
|
||||
|
||||
SequentialTask check_task(m_parent, tr("Checking for updates"));
|
||||
|
||||
if (!m_modrinth_to_update.empty()) {
|
||||
m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model);
|
||||
connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this,
|
||||
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
|
||||
check_task.addTask(m_modrinth_check_task);
|
||||
}
|
||||
|
||||
if (!m_flame_to_update.empty()) {
|
||||
m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model);
|
||||
connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this,
|
||||
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
|
||||
check_task.addTask(m_flame_check_task);
|
||||
}
|
||||
|
||||
connect(&check_task, &Task::failed, this,
|
||||
[&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
|
||||
|
||||
connect(&check_task, &Task::succeeded, this, [&]() {
|
||||
QStringList warnings = check_task.warnings();
|
||||
if (warnings.count()) {
|
||||
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec();
|
||||
}
|
||||
});
|
||||
|
||||
// Check for updates
|
||||
ProgressDialog progress_dialog(m_parent);
|
||||
progress_dialog.setSkipButton(true, tr("Abort"));
|
||||
progress_dialog.setWindowTitle(tr("Checking for updates..."));
|
||||
auto ret = progress_dialog.execWithTask(&check_task);
|
||||
|
||||
// If the dialog was skipped / some download error happened
|
||||
if (ret == QDialog::DialogCode::Rejected) {
|
||||
m_aborted = true;
|
||||
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add found updates for Modrinth
|
||||
if (m_modrinth_check_task) {
|
||||
auto modrinth_updates = m_modrinth_check_task->getUpdatable();
|
||||
for (auto& updatable : modrinth_updates) {
|
||||
qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
|
||||
|
||||
appendMod(updatable);
|
||||
m_tasks.insert(updatable.name, updatable.download);
|
||||
}
|
||||
}
|
||||
|
||||
// Add found updated for Flame
|
||||
if (m_flame_check_task) {
|
||||
auto flame_updates = m_flame_check_task->getUpdatable();
|
||||
for (auto& updatable : flame_updates) {
|
||||
qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
|
||||
|
||||
appendMod(updatable);
|
||||
m_tasks.insert(updatable.name, updatable.download);
|
||||
}
|
||||
}
|
||||
|
||||
// Report failed update checking
|
||||
if (!m_failed_check_update.empty()) {
|
||||
QString text;
|
||||
for (const auto& failed : m_failed_check_update) {
|
||||
const auto& mod = std::get<0>(failed);
|
||||
const auto& reason = std::get<1>(failed);
|
||||
const auto& recover_url = std::get<2>(failed);
|
||||
|
||||
qDebug() << mod->name() << " failed to check for updates!";
|
||||
|
||||
text += tr("Mod name: %1").arg(mod->name()) + "<br>";
|
||||
if (!reason.isEmpty())
|
||||
text += tr("Reason: %1").arg(reason) + "<br>";
|
||||
if (!recover_url.isEmpty())
|
||||
text += tr("Possible solution: ") + tr("Getting the latest version manually:") + "<br>" +
|
||||
QString("<a href='%1'>").arg(recover_url.toString()) + recover_url.toString() + "</a><br>";
|
||||
text += "<br>";
|
||||
}
|
||||
|
||||
ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"),
|
||||
tr("Could not check or get the following mods for updates:<br>"
|
||||
"Do you wish to proceed without those mods?"),
|
||||
text);
|
||||
message_dialog.setModal(true);
|
||||
if (message_dialog.exec() == QDialog::Rejected) {
|
||||
m_aborted = true;
|
||||
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no mod to be updated
|
||||
if (ui->modTreeWidget->topLevelItemCount() == 0) {
|
||||
m_no_updates = true;
|
||||
} else {
|
||||
// FIXME: Find a more efficient way of doing this!
|
||||
|
||||
// Sort major items in alphabetical order (also sorts the children unfortunately)
|
||||
ui->modTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder);
|
||||
|
||||
// Re-sort the children
|
||||
auto* item = ui->modTreeWidget->topLevelItem(0);
|
||||
for (int i = 1; item != nullptr; ++i) {
|
||||
item->sortChildren(0, Qt::SortOrder::DescendingOrder);
|
||||
item = ui->modTreeWidget->topLevelItem(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_aborted || m_no_updates)
|
||||
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
// Part 1: Ensure we have a valid metadata
|
||||
auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
{
|
||||
auto index_dir = indexDir();
|
||||
|
||||
SequentialTask seq(m_parent, tr("Looking for metadata"));
|
||||
|
||||
// A better use of data structures here could remove the need for this QHash
|
||||
QHash<QString, bool> should_try_others;
|
||||
QList<Mod*> modrinth_tmp;
|
||||
QList<Mod*> flame_tmp;
|
||||
|
||||
bool confirm_rest = false;
|
||||
bool try_others_rest = false;
|
||||
bool skip_rest = false;
|
||||
ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH;
|
||||
|
||||
auto addToTmp = [&](Mod* m, ModPlatform::Provider p) {
|
||||
switch (p) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
modrinth_tmp.push_back(m);
|
||||
break;
|
||||
case ModPlatform::Provider::FLAME:
|
||||
flame_tmp.push_back(m);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for (auto candidate : m_candidates) {
|
||||
auto* candidate_ptr = candidate.get();
|
||||
if (candidate->status() != ModStatus::NoMetadata) {
|
||||
onMetadataEnsured(candidate_ptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (skip_rest)
|
||||
continue;
|
||||
|
||||
if (confirm_rest) {
|
||||
addToTmp(candidate_ptr, provider_rest);
|
||||
should_try_others.insert(candidate->internal_id(), try_others_rest);
|
||||
continue;
|
||||
}
|
||||
|
||||
ChooseProviderDialog chooser(this);
|
||||
chooser.setDescription(tr("This mod (%1) does not have a metadata yet. We need to create one in order to keep relevant "
|
||||
"information on how to update this "
|
||||
"mod. To do this, please select a mod provider from which we can search for updates for %1.")
|
||||
.arg(candidate->name()));
|
||||
auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted;
|
||||
|
||||
auto response = chooser.getResponse();
|
||||
|
||||
if (response.skip_all)
|
||||
skip_rest = true;
|
||||
if (response.confirm_all) {
|
||||
confirm_rest = true;
|
||||
provider_rest = response.chosen;
|
||||
try_others_rest = response.try_others;
|
||||
}
|
||||
|
||||
should_try_others.insert(candidate->internal_id(), response.try_others);
|
||||
|
||||
if (confirmed)
|
||||
addToTmp(candidate_ptr, response.chosen);
|
||||
}
|
||||
|
||||
if (!modrinth_tmp.empty()) {
|
||||
auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH);
|
||||
connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
|
||||
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
|
||||
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
|
||||
});
|
||||
seq.addTask(modrinth_task);
|
||||
}
|
||||
|
||||
if (!flame_tmp.empty()) {
|
||||
auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME);
|
||||
connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
|
||||
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
|
||||
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
|
||||
});
|
||||
seq.addTask(flame_task);
|
||||
}
|
||||
|
||||
seq.addTask(m_second_try_metadata);
|
||||
|
||||
ProgressDialog checking_dialog(m_parent);
|
||||
checking_dialog.setSkipButton(true, tr("Abort"));
|
||||
checking_dialog.setWindowTitle(tr("Generating metadata..."));
|
||||
auto ret_metadata = checking_dialog.execWithTask(&seq);
|
||||
|
||||
return (ret_metadata != QDialog::DialogCode::Rejected);
|
||||
}
|
||||
|
||||
void ModUpdateDialog::onMetadataEnsured(Mod* mod)
|
||||
{
|
||||
// When the mod is a folder, for instance
|
||||
if (!mod->metadata())
|
||||
return;
|
||||
|
||||
switch (mod->metadata()->provider) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
m_modrinth_to_update.push_back(mod);
|
||||
break;
|
||||
case ModPlatform::Provider::FLAME:
|
||||
m_flame_to_update.push_back(mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ModPlatform::Provider next(ModPlatform::Provider p)
|
||||
{
|
||||
switch (p) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
return ModPlatform::Provider::FLAME;
|
||||
case ModPlatform::Provider::FLAME:
|
||||
return ModPlatform::Provider::MODRINTH;
|
||||
}
|
||||
|
||||
return ModPlatform::Provider::FLAME;
|
||||
}
|
||||
|
||||
void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice)
|
||||
{
|
||||
if (try_others) {
|
||||
auto index_dir = indexDir();
|
||||
|
||||
auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice));
|
||||
connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
|
||||
connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); });
|
||||
|
||||
m_second_try_metadata->addTask(task);
|
||||
} else {
|
||||
QString reason{ tr("Didn't find a valid version on the selected mod provider(s)") };
|
||||
|
||||
m_failed_metadata.append({mod, reason});
|
||||
}
|
||||
}
|
||||
|
||||
void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
|
||||
{
|
||||
auto item_top = new QTreeWidgetItem(ui->modTreeWidget);
|
||||
item_top->setCheckState(0, Qt::CheckState::Checked);
|
||||
item_top->setText(0, info.name);
|
||||
item_top->setExpanded(true);
|
||||
|
||||
auto provider_item = new QTreeWidgetItem(item_top);
|
||||
provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider)));
|
||||
|
||||
auto old_version_item = new QTreeWidgetItem(item_top);
|
||||
old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version));
|
||||
|
||||
auto new_version_item = new QTreeWidgetItem(item_top);
|
||||
new_version_item->setText(0, tr("New version: %1").arg(info.new_version));
|
||||
|
||||
auto changelog_item = new QTreeWidgetItem(item_top);
|
||||
changelog_item->setText(0, tr("Changelog of the latest version"));
|
||||
|
||||
auto changelog = new QTreeWidgetItem(changelog_item);
|
||||
auto changelog_area = new QTextBrowser();
|
||||
|
||||
switch (info.provider) {
|
||||
case ModPlatform::Provider::MODRINTH: {
|
||||
HoeDown h;
|
||||
// HoeDown bug?: \n aren't converted to <br>
|
||||
auto text = h.process(info.changelog.toUtf8());
|
||||
|
||||
// Don't convert if there's an HTML tag right after (Qt rendering weirdness)
|
||||
text.remove(QRegularExpression("(\n+)(?=<)"));
|
||||
text.replace('\n', "<br>");
|
||||
|
||||
changelog_area->setHtml(text);
|
||||
break;
|
||||
}
|
||||
case ModPlatform::Provider::FLAME: {
|
||||
changelog_area->setHtml(info.changelog);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
changelog_area->setOpenExternalLinks(true);
|
||||
changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::NoWrap);
|
||||
changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
|
||||
|
||||
// HACK: Is there a better way of achieving this?
|
||||
auto font_height = QFontMetrics(changelog_area->font()).height();
|
||||
changelog_area->setMaximumHeight((changelog_area->toPlainText().count(QRegularExpression("\n|<br>")) + 2) * font_height);
|
||||
|
||||
ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area);
|
||||
|
||||
ui->modTreeWidget->addTopLevelItem(item_top);
|
||||
}
|
||||
|
||||
auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*>
|
||||
{
|
||||
QList<ModDownloadTask*> list;
|
||||
|
||||
auto* item = ui->modTreeWidget->topLevelItem(0);
|
||||
|
||||
for (int i = 1; item != nullptr; ++i) {
|
||||
if (item->checkState(0) == Qt::CheckState::Checked) {
|
||||
list.push_back(m_tasks.find(item->text(0)).value());
|
||||
}
|
||||
|
||||
item = ui->modTreeWidget->topLevelItem(i);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
62
launcher/ui/dialogs/ModUpdateDialog.h
Normal file
62
launcher/ui/dialogs/ModUpdateDialog.h
Normal file
@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "BaseInstance.h"
|
||||
#include "ModDownloadTask.h"
|
||||
#include "ReviewMessageBox.h"
|
||||
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
|
||||
#include "modplatform/CheckUpdateTask.h"
|
||||
|
||||
class Mod;
|
||||
class ModrinthCheckUpdate;
|
||||
class FlameCheckUpdate;
|
||||
class ConcurrentTask;
|
||||
|
||||
class ModUpdateDialog final : public ReviewMessageBox {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModUpdateDialog(QWidget* parent,
|
||||
BaseInstance* instance,
|
||||
const std::shared_ptr<ModFolderModel> mod_model,
|
||||
QList<Mod::Ptr>& search_for);
|
||||
|
||||
void checkCandidates();
|
||||
|
||||
void appendMod(const CheckUpdateTask::UpdatableMod& info);
|
||||
|
||||
const QList<ModDownloadTask*> getTasks();
|
||||
auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
|
||||
|
||||
auto noUpdates() const -> bool { return m_no_updates; };
|
||||
auto aborted() const -> bool { return m_aborted; };
|
||||
|
||||
private:
|
||||
auto ensureMetadata() -> bool;
|
||||
|
||||
private slots:
|
||||
void onMetadataEnsured(Mod*);
|
||||
void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH);
|
||||
|
||||
private:
|
||||
QWidget* m_parent;
|
||||
|
||||
ModrinthCheckUpdate* m_modrinth_check_task = nullptr;
|
||||
FlameCheckUpdate* m_flame_check_task = nullptr;
|
||||
|
||||
const std::shared_ptr<ModFolderModel> m_mod_model;
|
||||
|
||||
QList<Mod::Ptr>& m_candidates;
|
||||
QList<Mod*> m_modrinth_to_update;
|
||||
QList<Mod*> m_flame_to_update;
|
||||
|
||||
ConcurrentTask* m_second_try_metadata;
|
||||
QList<std::tuple<Mod*, QString>> m_failed_metadata;
|
||||
QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update;
|
||||
|
||||
QHash<QString, ModDownloadTask*> m_tasks;
|
||||
BaseInstance* m_instance;
|
||||
|
||||
bool m_no_updates = false;
|
||||
bool m_aborted = false;
|
||||
};
|
@ -62,24 +62,24 @@ void ProgressDialog::updateSize()
|
||||
int ProgressDialog::execWithTask(Task* task)
|
||||
{
|
||||
this->task = task;
|
||||
QDialog::DialogCode result;
|
||||
|
||||
if (!task) {
|
||||
qDebug() << "Programmer error: progress dialog created with null task.";
|
||||
return Accepted;
|
||||
qDebug() << "Programmer error: Progress dialog created with null task.";
|
||||
return QDialog::DialogCode::Accepted;
|
||||
}
|
||||
|
||||
QDialog::DialogCode result;
|
||||
if (handleImmediateResult(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Connect signals.
|
||||
connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
|
||||
connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
|
||||
connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
|
||||
connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&)));
|
||||
connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&)));
|
||||
connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64)));
|
||||
connect(task, &Task::started, this, &ProgressDialog::onTaskStarted);
|
||||
connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed);
|
||||
connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded);
|
||||
connect(task, &Task::status, this, &ProgressDialog::changeStatus);
|
||||
connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus);
|
||||
connect(task, &Task::progress, this, &ProgressDialog::changeProgress);
|
||||
|
||||
connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); });
|
||||
|
||||
@ -89,19 +89,15 @@ int ProgressDialog::execWithTask(Task* task)
|
||||
ui->globalProgressBar->setHidden(true);
|
||||
}
|
||||
|
||||
// if this didn't connect to an already running task, invoke start
|
||||
// It's a good idea to start the task after we entered the dialog's event loop :^)
|
||||
if (!task->isRunning()) {
|
||||
task->start();
|
||||
}
|
||||
if (task->isRunning()) {
|
||||
changeProgress(task->getProgress(), task->getTotalProgress());
|
||||
changeStatus(task->getStatus());
|
||||
return QDialog::exec();
|
||||
} else if (handleImmediateResult(result)) {
|
||||
return result;
|
||||
QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection);
|
||||
} else {
|
||||
return QDialog::Rejected;
|
||||
changeStatus(task->getStatus());
|
||||
changeProgress(task->getProgress(), task->getTotalProgress());
|
||||
}
|
||||
|
||||
return QDialog::exec();
|
||||
}
|
||||
|
||||
// TODO: only provide the unique_ptr overloads
|
||||
|
@ -40,7 +40,7 @@ auto ReviewMessageBox::deselectedMods() -> QStringList
|
||||
|
||||
auto* item = ui->modTreeWidget->topLevelItem(0);
|
||||
|
||||
for (int i = 0; item != nullptr; ++i) {
|
||||
for (int i = 1; item != nullptr; ++i) {
|
||||
if (item->checkState(0) == Qt::CheckState::Unchecked) {
|
||||
list.append(item->text(0));
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<width>500</width>
|
||||
<height>455</height>
|
||||
</rect>
|
||||
</property>
|
||||
|
@ -147,6 +147,17 @@
|
||||
<string>Download a new resource</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionUpdateItem">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Check for &Updates</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>"Tries to find / update all selected resources (all resources if none is selected)"</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
@ -49,6 +49,7 @@
|
||||
#include "ui/GuiUtil.h"
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui/dialogs/ModDownloadDialog.h"
|
||||
#include "ui/dialogs/ModUpdateDialog.h"
|
||||
|
||||
#include "DesktopServices.h"
|
||||
|
||||
@ -78,6 +79,23 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
|
||||
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
|
||||
|
||||
connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods);
|
||||
|
||||
ui->actionUpdateItem->setToolTip(tr("Tries to find / update all selected mods (all mods if none is selected)"));
|
||||
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
|
||||
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
|
||||
|
||||
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
[this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
|
||||
|
||||
connect(mods.get(), &ModFolderModel::rowsInserted, this,
|
||||
[this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
|
||||
|
||||
connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] {
|
||||
ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
|
||||
|
||||
// Prevent a weird crash when trying to open the mods page twice in a session o.O
|
||||
disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +125,6 @@ bool CoreModFolderPage::shouldDisplay() const
|
||||
return false;
|
||||
if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -118,7 +135,7 @@ void ModFolderPage::installMods()
|
||||
return;
|
||||
if (m_instance->typeName() != "Minecraft")
|
||||
return; // this is a null instance or a legacy instance
|
||||
|
||||
|
||||
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
|
||||
if (profile->getModLoaders() == ModAPI::Unspecified) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
|
||||
@ -140,7 +157,7 @@ void ModFolderPage::installMods()
|
||||
QStringList warnings = tasks->warnings();
|
||||
if (warnings.count())
|
||||
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
|
||||
|
||||
|
||||
tasks->deleteLater();
|
||||
});
|
||||
|
||||
@ -155,3 +172,58 @@ void ModFolderPage::installMods()
|
||||
m_model->update();
|
||||
}
|
||||
}
|
||||
|
||||
void ModFolderPage::updateMods()
|
||||
{
|
||||
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
|
||||
|
||||
auto mods_list = m_model->selectedMods(selection);
|
||||
bool use_all = mods_list.empty();
|
||||
if (use_all)
|
||||
mods_list = m_model->allMods();
|
||||
|
||||
ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list);
|
||||
update_dialog.checkCandidates();
|
||||
|
||||
if (update_dialog.aborted()) {
|
||||
CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show();
|
||||
return;
|
||||
}
|
||||
if (update_dialog.noUpdates()) {
|
||||
CustomMessageBox::selectable(this, tr("Update checker"),
|
||||
(mods_list.size() == 1)
|
||||
? tr("'%1' is up-to-date! :)").arg(mods_list.front()->name())
|
||||
: tr("All %1mods are up-to-date! :)").arg(use_all ? "" : (tr("selected") + " ")))
|
||||
->exec();
|
||||
return;
|
||||
}
|
||||
|
||||
if (update_dialog.exec()) {
|
||||
ConcurrentTask* tasks = new ConcurrentTask(this);
|
||||
connect(tasks, &Task::failed, [this, tasks](QString reason) {
|
||||
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
|
||||
tasks->deleteLater();
|
||||
});
|
||||
connect(tasks, &Task::aborted, [this, tasks]() {
|
||||
CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show();
|
||||
tasks->deleteLater();
|
||||
});
|
||||
connect(tasks, &Task::succeeded, [this, tasks]() {
|
||||
QStringList warnings = tasks->warnings();
|
||||
if (warnings.count()) {
|
||||
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
|
||||
}
|
||||
tasks->deleteLater();
|
||||
});
|
||||
|
||||
for (auto task : update_dialog.getTasks()) {
|
||||
tasks->addTask(task);
|
||||
}
|
||||
|
||||
ProgressDialog loadDialog(this);
|
||||
loadDialog.setSkipButton(true, tr("Abort"));
|
||||
loadDialog.execWithTask(tasks);
|
||||
|
||||
m_model->update();
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ class ModFolderPage : public ExternalResourcesPage {
|
||||
|
||||
private slots:
|
||||
void installMods();
|
||||
void updateMods();
|
||||
};
|
||||
|
||||
class CoreModFolderPage : public ModFolderPage {
|
||||
|
@ -64,7 +64,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
|
||||
{
|
||||
Q_UNUSED(loaders);
|
||||
return ver.mcVersion.contains(mineVer);
|
||||
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
|
||||
}
|
||||
|
||||
// I don't know why, but doing this on the parent class makes it so that
|
||||
|
@ -76,13 +76,20 @@ void WideBar::addSeparator()
|
||||
m_entries.push_back(entry);
|
||||
}
|
||||
|
||||
void WideBar::insertActionBefore(QAction* before, QAction* action){
|
||||
auto iter = std::find_if(m_entries.begin(), m_entries.end(), [before](BarEntry * entry) {
|
||||
return entry->wideAction == before;
|
||||
auto WideBar::getMatching(QAction* act) -> QList<BarEntry*>::iterator
|
||||
{
|
||||
auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry * entry) {
|
||||
return entry->wideAction == act;
|
||||
});
|
||||
if(iter == m_entries.end()) {
|
||||
|
||||
return iter;
|
||||
}
|
||||
|
||||
void WideBar::insertActionBefore(QAction* before, QAction* action){
|
||||
auto iter = getMatching(before);
|
||||
if(iter == m_entries.end())
|
||||
return;
|
||||
}
|
||||
|
||||
auto entry = new BarEntry();
|
||||
entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this));
|
||||
entry->wideAction = action;
|
||||
@ -90,14 +97,24 @@ void WideBar::insertActionBefore(QAction* before, QAction* action){
|
||||
m_entries.insert(iter, entry);
|
||||
}
|
||||
|
||||
void WideBar::insertActionAfter(QAction* after, QAction* action){
|
||||
auto iter = getMatching(after);
|
||||
if(iter == m_entries.end())
|
||||
return;
|
||||
|
||||
auto entry = new BarEntry();
|
||||
entry->qAction = insertWidget((*(iter+1))->qAction, new ActionButton(action, this));
|
||||
entry->wideAction = action;
|
||||
entry->type = BarEntry::Action;
|
||||
m_entries.insert(iter + 1, entry);
|
||||
}
|
||||
|
||||
void WideBar::insertSpacer(QAction* action)
|
||||
{
|
||||
auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) {
|
||||
return entry->wideAction == action;
|
||||
});
|
||||
if(iter == m_entries.end()) {
|
||||
auto iter = getMatching(action);
|
||||
if(iter == m_entries.end())
|
||||
return;
|
||||
}
|
||||
|
||||
QWidget* spacer = new QWidget();
|
||||
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
@ -107,6 +124,18 @@ void WideBar::insertSpacer(QAction* action)
|
||||
m_entries.insert(iter, entry);
|
||||
}
|
||||
|
||||
void WideBar::insertSeparator(QAction* before)
|
||||
{
|
||||
auto iter = getMatching(before);
|
||||
if(iter == m_entries.end())
|
||||
return;
|
||||
|
||||
auto entry = new BarEntry();
|
||||
entry->qAction = QToolBar::insertSeparator(before);
|
||||
entry->type = BarEntry::Separator;
|
||||
m_entries.insert(iter, entry);
|
||||
}
|
||||
|
||||
QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title)
|
||||
{
|
||||
QMenu *contextMenu = new QMenu(title, parent);
|
||||
|
@ -1,27 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QToolBar>
|
||||
#include <QAction>
|
||||
#include <QMap>
|
||||
#include <QToolBar>
|
||||
|
||||
class QMenu;
|
||||
|
||||
class WideBar : public QToolBar
|
||||
{
|
||||
class WideBar : public QToolBar {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WideBar(const QString &title, QWidget * parent = nullptr);
|
||||
explicit WideBar(QWidget * parent = nullptr);
|
||||
public:
|
||||
explicit WideBar(const QString& title, QWidget* parent = nullptr);
|
||||
explicit WideBar(QWidget* parent = nullptr);
|
||||
virtual ~WideBar();
|
||||
|
||||
void addAction(QAction *action);
|
||||
void addAction(QAction* action);
|
||||
void addSeparator();
|
||||
void insertSpacer(QAction *action);
|
||||
void insertActionBefore(QAction *before, QAction *action);
|
||||
QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString());
|
||||
|
||||
private:
|
||||
void insertSpacer(QAction* action);
|
||||
void insertSeparator(QAction* before);
|
||||
void insertActionBefore(QAction* before, QAction* action);
|
||||
void insertActionAfter(QAction* after, QAction* action);
|
||||
|
||||
QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString());
|
||||
|
||||
private:
|
||||
struct BarEntry;
|
||||
QList<BarEntry *> m_entries;
|
||||
|
||||
auto getMatching(QAction* act) -> QList<BarEntry*>::iterator;
|
||||
|
||||
private:
|
||||
QList<BarEntry*> m_entries;
|
||||
};
|
||||
|
@ -149,6 +149,12 @@ BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt
|
||||
|
||||
Changes are made to make the code more generic and useful in less usual conditions.
|
||||
|
||||
## murmur2
|
||||
|
||||
Canonical implementation of the murmur2 hash, taken from [SMHasher](https://github.com/aappleby/smhasher).
|
||||
|
||||
Public domain (the author disclaimed the copyright).
|
||||
|
||||
## optional-bare
|
||||
|
||||
A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later.
|
||||
|
12
libraries/murmur2/CMakeLists.txt
Normal file
12
libraries/murmur2/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.9.4)
|
||||
project(murmur2)
|
||||
|
||||
set(MURMUR_SOURCES
|
||||
src/MurmurHash2.h
|
||||
src/MurmurHash2.cpp
|
||||
)
|
||||
|
||||
add_library(Launcher_murmur2 STATIC ${MURMUR_SOURCES})
|
||||
target_include_directories(Launcher_murmur2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "src" )
|
||||
|
||||
generate_export_header(Launcher_murmur2)
|
86
libraries/murmur2/src/MurmurHash2.cpp
Normal file
86
libraries/murmur2/src/MurmurHash2.cpp
Normal file
@ -0,0 +1,86 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// MurmurHash2 was written by Austin Appleby, and is placed in the public
|
||||
// domain. The author hereby disclaims copyright to this source code.
|
||||
|
||||
// Note - This code makes a few assumptions about how your machine behaves -
|
||||
|
||||
// 1. We can read a 4-byte value from any address without crashing
|
||||
// 2. sizeof(int) == 4
|
||||
|
||||
// And it has a few limitations -
|
||||
|
||||
// 1. It will not work incrementally.
|
||||
// 2. It will not produce the same results on little-endian and big-endian
|
||||
// machines.
|
||||
|
||||
#include "MurmurHash2.h"
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Platform-specific functions and macros
|
||||
|
||||
// Microsoft Visual Studio
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
|
||||
#define BIG_CONSTANT(x) (x)
|
||||
|
||||
// Other compilers
|
||||
|
||||
#else // defined(_MSC_VER)
|
||||
|
||||
#define BIG_CONSTANT(x) (x##LLU)
|
||||
|
||||
#endif // !defined(_MSC_VER)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed )
|
||||
{
|
||||
// 'm' and 'r' are mixing constants generated offline.
|
||||
// They're not really 'magic', they just happen to work well.
|
||||
|
||||
const uint32_t m = 0x5bd1e995;
|
||||
const int r = 24;
|
||||
|
||||
// Initialize the hash to a 'random' value
|
||||
|
||||
uint32_t h = seed ^ len;
|
||||
|
||||
// Mix 4 bytes at a time into the hash
|
||||
const auto* data = (const unsigned char*) key;
|
||||
while(len >= 4)
|
||||
{
|
||||
uint32_t k = *(uint32_t*)data;
|
||||
|
||||
k *= m;
|
||||
k ^= k >> r;
|
||||
k *= m;
|
||||
|
||||
h *= m;
|
||||
h ^= k;
|
||||
|
||||
data += 4*sizeof(char);
|
||||
len -= 4;
|
||||
}
|
||||
|
||||
// Handle the last few bytes of the input array
|
||||
|
||||
switch(len)
|
||||
{
|
||||
case 3: h ^= data[2] << 16;
|
||||
case 2: h ^= data[1] << 8;
|
||||
case 1: h ^= data[0];
|
||||
h *= m;
|
||||
};
|
||||
|
||||
// Do a few final mixes of the hash to ensure the last few
|
||||
// bytes are well-incorporated.
|
||||
|
||||
h ^= h >> 13;
|
||||
h *= m;
|
||||
h ^= h >> 15;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
30
libraries/murmur2/src/MurmurHash2.h
Normal file
30
libraries/murmur2/src/MurmurHash2.h
Normal file
@ -0,0 +1,30 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// MurmurHash2 was written by Austin Appleby, and is placed in the public
|
||||
// domain. The author hereby disclaims copyright to this source code.
|
||||
|
||||
#pragma once
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Platform-specific functions and macros
|
||||
|
||||
// Microsoft Visual Studio
|
||||
|
||||
#if defined(_MSC_VER) && (_MSC_VER < 1600)
|
||||
|
||||
typedef unsigned char uint8_t;
|
||||
typedef unsigned int uint32_t;
|
||||
typedef unsigned __int64 uint64_t;
|
||||
|
||||
// Other compilers
|
||||
|
||||
#else // defined(_MSC_VER)
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#endif // !defined(_MSC_VER)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed = 1 );
|
||||
|
||||
//-----------------------------------------------------------------------------
|
Loading…
Reference in New Issue
Block a user