Merge branch 'develop' of https://github.com/PolyMC/PolyMC into rebase

This commit is contained in:
fn2006 2022-09-29 15:10:32 +01:00
commit 11f97a78fe
85 changed files with 2437 additions and 935 deletions

View File

@ -39,7 +39,7 @@ jobs:
qt_ver: 6 qt_ver: 6
- os: macos-12 - os: macos-12
macosx_deployment_target: 10.14 macosx_deployment_target: 10.15
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_version: '6.3.0' qt_version: '6.3.0'

View File

@ -1042,7 +1042,7 @@ void Application::performMainStartupAction()
qDebug() << " Launching with account" << m_profileToUse; qDebug() << " Launching with account" << m_profileToUse;
} }
launch(inst, true, nullptr, serverToJoin, accountToUse); launch(inst, true, false, nullptr, serverToJoin, accountToUse);
return; return;
} }
} }
@ -1146,6 +1146,7 @@ void Application::messageReceived(const QByteArray& message)
launch( launch(
instance, instance,
true, true,
false,
nullptr, nullptr,
serverObject, serverObject,
accountObject accountObject
@ -1247,6 +1248,7 @@ bool Application::openJsonEditor(const QString &filename)
bool Application::launch( bool Application::launch(
InstancePtr instance, InstancePtr instance,
bool online, bool online,
bool demo,
BaseProfilerFactory *profiler, BaseProfilerFactory *profiler,
MinecraftServerTargetPtr serverToJoin, MinecraftServerTargetPtr serverToJoin,
MinecraftAccountPtr accountToUse MinecraftAccountPtr accountToUse
@ -1270,6 +1272,7 @@ bool Application::launch(
controller.reset(new LaunchController()); controller.reset(new LaunchController());
controller->setInstance(instance); controller->setInstance(instance);
controller->setOnline(online); controller->setOnline(online);
controller->setDemo(demo);
controller->setProfiler(profiler); controller->setProfiler(profiler);
controller->setServerToJoin(serverToJoin); controller->setServerToJoin(serverToJoin);
controller->setAccountToUse(accountToUse); controller->setAccountToUse(accountToUse);

View File

@ -213,6 +213,7 @@ public slots:
bool launch( bool launch(
InstancePtr instance, InstancePtr instance,
bool online = true, bool online = true,
bool demo = false,
BaseProfilerFactory *profiler = nullptr, BaseProfilerFactory *profiler = nullptr,
MinecraftServerTargetPtr serverToJoin = nullptr, MinecraftServerTargetPtr serverToJoin = nullptr,
MinecraftAccountPtr accountToUse = nullptr MinecraftAccountPtr accountToUse = nullptr

View File

@ -114,44 +114,54 @@ QString BaseInstance::getPostExitCommand()
return settings()->get("PostExitCommand").toString(); return settings()->get("PostExitCommand").toString();
} }
bool BaseInstance::isManagedPack() bool BaseInstance::isManagedPack() const
{ {
return settings()->get("ManagedPack").toBool(); return m_settings->get("ManagedPack").toBool();
} }
QString BaseInstance::getManagedPackType() QString BaseInstance::getManagedPackType() const
{ {
return settings()->get("ManagedPackType").toString(); return m_settings->get("ManagedPackType").toString();
} }
QString BaseInstance::getManagedPackID() QString BaseInstance::getManagedPackID() const
{ {
return settings()->get("ManagedPackID").toString(); return m_settings->get("ManagedPackID").toString();
} }
QString BaseInstance::getManagedPackName() QString BaseInstance::getManagedPackName() const
{ {
return settings()->get("ManagedPackName").toString(); return m_settings->get("ManagedPackName").toString();
} }
QString BaseInstance::getManagedPackVersionID() QString BaseInstance::getManagedPackVersionID() const
{ {
return settings()->get("ManagedPackVersionID").toString(); return m_settings->get("ManagedPackVersionID").toString();
} }
QString BaseInstance::getManagedPackVersionName() QString BaseInstance::getManagedPackVersionName() const
{ {
return settings()->get("ManagedPackVersionName").toString(); return m_settings->get("ManagedPackVersionName").toString();
} }
void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version)
{ {
settings()->set("ManagedPack", true); m_settings->set("ManagedPack", true);
settings()->set("ManagedPackType", type); m_settings->set("ManagedPackType", type);
settings()->set("ManagedPackID", id); m_settings->set("ManagedPackID", id);
settings()->set("ManagedPackName", name); m_settings->set("ManagedPackName", name);
settings()->set("ManagedPackVersionID", versionId); m_settings->set("ManagedPackVersionID", versionId);
settings()->set("ManagedPackVersionName", version); m_settings->set("ManagedPackVersionName", version);
}
void BaseInstance::copyManagedPack(BaseInstance& other)
{
m_settings->set("ManagedPack", other.isManagedPack());
m_settings->set("ManagedPackType", other.getManagedPackType());
m_settings->set("ManagedPackID", other.getManagedPackID());
m_settings->set("ManagedPackName", other.getManagedPackName());
m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID());
m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName());
} }
int BaseInstance::getConsoleMaxLines() const int BaseInstance::getConsoleMaxLines() const

View File

@ -140,13 +140,14 @@ public:
QString getPostExitCommand(); QString getPostExitCommand();
QString getWrapperCommand(); QString getWrapperCommand();
bool isManagedPack(); bool isManagedPack() const;
QString getManagedPackType(); QString getManagedPackType() const;
QString getManagedPackID(); QString getManagedPackID() const;
QString getManagedPackName(); QString getManagedPackName() const;
QString getManagedPackVersionID(); QString getManagedPackVersionID() const;
QString getManagedPackVersionName(); QString getManagedPackVersionName() const;
void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version);
void copyManagedPack(BaseInstance& other);
/// guess log level from a line of game log /// guess log level from a line of game log
virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level)

View File

@ -305,6 +305,8 @@ set(MINECRAFT_SOURCES
minecraft/Library.cpp minecraft/Library.cpp
minecraft/Library.h minecraft/Library.h
minecraft/MojangDownloadInfo.h minecraft/MojangDownloadInfo.h
minecraft/VanillaInstanceCreationTask.cpp
minecraft/VanillaInstanceCreationTask.h
minecraft/VersionFile.cpp minecraft/VersionFile.cpp
minecraft/VersionFile.h minecraft/VersionFile.h
minecraft/VersionFilterData.h minecraft/VersionFilterData.h
@ -328,6 +330,8 @@ set(MINECRAFT_SOURCES
minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePack.h
minecraft/mod/TexturePack.cpp
minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/ShaderPackFolderModel.h minecraft/mod/ShaderPackFolderModel.h
@ -340,6 +344,8 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalModUpdateTask.cpp minecraft/mod/tasks/LocalModUpdateTask.cpp
minecraft/mod/tasks/LocalResourcePackParseTask.h minecraft/mod/tasks/LocalResourcePackParseTask.h
minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalResourcePackParseTask.cpp
minecraft/mod/tasks/LocalTexturePackParseTask.h
minecraft/mod/tasks/LocalTexturePackParseTask.cpp
# Assets # Assets
minecraft/AssetsUtils.h minecraft/AssetsUtils.h
@ -463,6 +469,8 @@ set(API_SOURCES
modplatform/helpers/NetworkModAPI.cpp modplatform/helpers/NetworkModAPI.cpp
modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
modplatform/helpers/OverrideUtils.cpp
) )
set(FTB_SOURCES set(FTB_SOURCES
@ -488,6 +496,8 @@ set(FLAME_SOURCES
modplatform/flame/FileResolvingTask.cpp modplatform/flame/FileResolvingTask.cpp
modplatform/flame/FlameCheckUpdate.cpp modplatform/flame/FlameCheckUpdate.cpp
modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp
) )
set(MODRINTH_SOURCES set(MODRINTH_SOURCES
@ -497,6 +507,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthPackManifest.h modplatform/modrinth/ModrinthPackManifest.h
modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.cpp
modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
) )
set(MODPACKSCH_SOURCES set(MODPACKSCH_SOURCES

View File

@ -37,6 +37,7 @@
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QDirIterator>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QSaveFile> #include <QSaveFile>
@ -58,6 +59,24 @@
#include <utime.h> #include <utime.h>
#endif #endif
#include <filesystem>
#if defined Q_OS_WIN32
std::wstring toStdString(QString s)
{
return s.toStdWString();
}
#else
std::string toStdString(QString s)
{
return s.toStdString();
}
#endif
namespace FS { namespace FS {
void ensureExists(const QDir& dir) void ensureExists(const QDir& dir)
@ -128,6 +147,8 @@ bool ensureFolderPathExists(QString foldernamepath)
bool copy::operator()(const QString& offset) bool copy::operator()(const QString& offset)
{ {
using copy_opts = std::filesystem::copy_options;
// NOTE always deep copy on windows. the alternatives are too messy. // NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
m_followSymlinks = true; m_followSymlinks = true;
@ -136,94 +157,53 @@ bool copy::operator()(const QString& offset)
auto src = PathCombine(m_src.absolutePath(), offset); auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset);
QFileInfo currentSrc(src); std::error_code err;
if (!currentSrc.exists())
return false;
if (!m_followSymlinks && currentSrc.isSymLink()) { std::filesystem::copy_options opt = copy_opts::none;
qDebug() << "creating symlink" << src << " - " << dst;
if (!ensureFilePathExists(dst)) { // The default behavior is to follow symlinks
qWarning() << "Cannot create path!"; if (!m_followSymlinks)
return false; opt |= copy_opts::copy_symlinks;
// We can't use copy_opts::recursive because we need to take into account the
// blacklisted paths, so we iterate over the source directory, and if there's no blacklist
// match, we copy the file.
QDir src_dir(src);
QDirIterator source_it(src, QDir::Filter::Files, QDirIterator::Subdirectories);
while (source_it.hasNext()) {
auto src_path = source_it.next();
auto relative_path = src_dir.relativeFilePath(src_path);
if (m_blacklist && m_blacklist->matches(relative_path))
continue;
auto dst_path = PathCombine(dst, relative_path);
ensureFilePathExists(dst_path);
std::filesystem::copy(toStdString(src_path), toStdString(dst_path), opt, err);
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
} }
return QFile::link(currentSrc.symLinkTarget(), dst);
} else if (currentSrc.isFile()) {
qDebug() << "copying file" << src << " - " << dst;
if (!ensureFilePathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
return QFile::copy(src, dst);
} else if (currentSrc.isDir()) {
qDebug() << "recursing" << offset;
if (!ensureFolderPathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
QDir currentDir(src);
for (auto& f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) {
auto inner_offset = PathCombine(offset, f);
// ignore and skip stuff that matches the blacklist.
if (m_blacklist && m_blacklist->matches(inner_offset)) {
continue;
}
if (!operator()(inner_offset)) {
qWarning() << "Failed to copy" << inner_offset;
return false;
}
}
} else {
qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
return false;
} }
return true;
return err.value() == 0;
} }
bool deletePath(QString path) bool deletePath(QString path)
{ {
bool OK = true; std::error_code err;
QFileInfo finfo(path);
if (finfo.isFile()) { std::filesystem::remove_all(toStdString(path), err);
return QFile::remove(path);
if (err) {
qWarning() << "Failed to remove files:" << QString::fromStdString(err.message());
} }
QDir dir(path); return err.value() == 0;
if (!dir.exists()) {
return OK;
}
auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst);
for (auto& info : allEntries) {
#if defined Q_OS_WIN32
QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath());
auto wString = nativePath.toStdWString();
DWORD dwAttrs = GetFileAttributesW(wString.c_str());
// Windows: check for junctions, reparse points and other nasty things of that sort
if (dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) {
if (info.isFile()) {
OK &= QFile::remove(info.absoluteFilePath());
} else if (info.isDir()) {
OK &= dir.rmdir(info.absoluteFilePath());
}
}
#else
// We do not trust Qt with reparse points, but do trust it with unix symlinks.
if (info.isSymLink()) {
OK &= QFile::remove(info.absoluteFilePath());
}
#endif
else if (info.isDir()) {
OK &= deletePath(info.absoluteFilePath());
} else if (info.isFile()) {
OK &= QFile::remove(info.absoluteFilePath());
} else {
OK = false;
qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath();
}
}
OK &= dir.rmdir(dir.absolutePath());
return OK;
} }
bool trash(QString path, QString *pathInTrash = nullptr) bool trash(QString path, QString *pathInTrash = nullptr)
@ -316,8 +296,7 @@ QString DirNameFromString(QString string, QString inDir)
if (num == 0) { if (num == 0) {
dirName = baseName; dirName = baseName;
} else { } else {
dirName = baseName + QString::number(num); dirName = baseName + "(" + QString::number(num) + ")";
;
} }
// If it's over 9000 // If it's over 9000
@ -336,50 +315,6 @@ bool checkProblemticPathJava(QDir folder)
return pathfoldername.contains("!", Qt::CaseInsensitive); return pathfoldername.contains("!", Qt::CaseInsensitive);
} }
// Win32 crap
#ifdef Q_OS_WIN
bool called_coinit = false;
HRESULT CreateLink(LPCCH linkPath, LPCWSTR targetPath, LPCWSTR args)
{
HRESULT hres;
if (!called_coinit) {
hres = CoInitialize(NULL);
called_coinit = true;
if (!SUCCEEDED(hres)) {
qWarning("Failed to initialize COM. Error 0x%08lX", hres);
return hres;
}
}
IShellLink* link;
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&link);
if (SUCCEEDED(hres)) {
IPersistFile* persistFile;
link->SetPath(targetPath);
link->SetArguments(args);
hres = link->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile);
if (SUCCEEDED(hres)) {
WCHAR wstr[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH);
hres = persistFile->Save(wstr, TRUE);
persistFile->Release();
}
link->Release();
}
return hres;
}
#endif
QString getDesktopDir() QString getDesktopDir()
{ {
return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
@ -439,47 +374,24 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
#endif #endif
} }
QStringList listFolderPaths(QDir root)
{
auto createAbsPath = [](QFileInfo const& entry) { return FS::PathCombine(entry.path(), entry.fileName()); };
QStringList entries;
root.refresh();
for (auto entry : root.entryInfoList(QDir::Filter::Files)) {
entries.append(createAbsPath(entry));
}
for (auto entry : root.entryInfoList(QDir::Filter::AllDirs | QDir::Filter::NoDotAndDotDot)) {
entries.append(listFolderPaths(createAbsPath(entry)));
}
return entries;
}
bool overrideFolder(QString overwritten_path, QString override_path) bool overrideFolder(QString overwritten_path, QString override_path)
{ {
using copy_opts = std::filesystem::copy_options;
if (!FS::ensureFolderPathExists(overwritten_path)) if (!FS::ensureFolderPathExists(overwritten_path))
return false; return false;
QStringList paths_to_override; std::error_code err;
QDir root_override (override_path); std::filesystem::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing;
for (auto file : listFolderPaths(root_override)) {
QString destination = file;
destination.replace(override_path, overwritten_path);
ensureFilePathExists(destination);
qDebug() << QString("Applying override %1 in %2").arg(file, destination); std::filesystem::copy(toStdString(override_path), toStdString(overwritten_path), opt, err);
if (QFile::exists(destination)) if (err) {
QFile::remove(destination); qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path);
if (!QFile::rename(file, destination)) { qCritical() << "Reason:" << QString::fromStdString(err.message());
qCritical() << QString("Failed to apply override from %1 to %2").arg(file, destination);
return false;
}
} }
return true; return err.value() == 0;
} }
} }

View File

@ -44,7 +44,7 @@ void InstanceCopyTask::copyFinished()
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(m_instName); inst->setName(name());
inst->setIconKey(m_instIcon); inst->setIconKey(m_instIcon);
if(!m_keepPlaytime) { if(!m_keepPlaytime) {
inst->resetTimePlayed(); inst->resetTimePlayed();

View File

@ -1,40 +1,56 @@
#include "InstanceCreationTask.h" #include "InstanceCreationTask.h"
#include "settings/INISettingsObject.h"
#include "FileSystem.h"
//FIXME: remove this #include <QDebug>
#include "minecraft/MinecraftInstance.h" #include <QFile>
#include "minecraft/PackProfile.h"
InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version) InstanceCreationTask::InstanceCreationTask() = default;
{
m_version = version;
m_usingLoader = false;
}
InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion)
{
m_version = version;
m_usingLoader = true;
m_loader = loader;
m_loaderVersion = loaderVersion;
}
void InstanceCreationTask::executeTask() void InstanceCreationTask::executeTask()
{ {
setStatus(tr("Creating instance from version %1").arg(m_version->name())); setAbortable(true);
{
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); if (updateInstance()) {
instanceSettings->suspendSave(); emitSucceeded();
MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); return;
auto components = inst.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
if(m_usingLoader)
components->setComponentVersion(m_loader, m_loaderVersion->descriptor());
inst.setName(m_instName);
inst.setIconKey(m_instIcon);
instanceSettings->resumeSave();
} }
// When the user aborted in the update stage.
if (m_abort) {
emitAborted();
return;
}
if (!createInstance()) {
if (m_abort)
return;
qWarning() << "Instance creation failed!";
if (!m_error_message.isEmpty())
qWarning() << "Reason: " << m_error_message;
emitFailed(tr("Error while creating new instance."));
return;
}
// If this is set, it means we're updating an instance. So, we now need to remove the
// files scheduled to, and we'd better not let the user abort in the middle of it, since it'd
// put the instance in an invalid state.
if (shouldOverride()) {
setAbortable(false);
setStatus(tr("Removing old conflicting files..."));
qDebug() << "Removing old files";
for (auto path : m_files_to_remove) {
if (!QFile::exists(path))
continue;
qDebug() << "Removing" << path;
if (!QFile::remove(path)) {
qCritical() << "Couldn't remove the old conflicting files.";
emitFailed(tr("Failed to remove old conflicting files."));
return;
}
}
}
emitSucceeded(); emitSucceeded();
return;
} }

View File

@ -1,26 +1,46 @@
#pragma once #pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include <QUrl>
#include "settings/SettingsObject.h"
#include "BaseVersion.h" #include "BaseVersion.h"
#include "InstanceTask.h" #include "InstanceTask.h"
class InstanceCreationTask : public InstanceTask class InstanceCreationTask : public InstanceTask {
{
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceCreationTask(BaseVersionPtr version); InstanceCreationTask();
explicit InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion); virtual ~InstanceCreationTask() = default;
protected: protected:
//! Entry point for tasks. void executeTask() final override;
virtual void executeTask() override;
private: /* data */ /**
BaseVersionPtr m_version; * Tries to update an already existing instance.
bool m_usingLoader; *
QString m_loader; * This can be implemented by subclasses to provide a way of updating an already existing
BaseVersionPtr m_loaderVersion; * instance, according to that implementation's concept of 'identity' (i.e. instances that
* are updates / downgrades of one another).
*
* If this returns true, createInstance() will not run, so you should do all update steps in here.
* Otherwise, createInstance() is run as normal.
*/
virtual bool updateInstance() { return false; };
/**
* Creates a new instance.
*
* Returns whether the instance creation was successful (true) or not (false).
*/
virtual bool createInstance() { return false; };
QString getError() const { return m_error_message; }
protected:
void setError(QString message) { m_error_message = message; };
protected:
bool m_abort = false;
QStringList m_files_to_remove;
private:
QString m_error_message;
}; };

View File

@ -35,35 +35,26 @@
*/ */
#include "InstanceImportTask.h" #include "InstanceImportTask.h"
#include <QtConcurrentRun>
#include "Application.h" #include "Application.h"
#include "BaseInstance.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "MMCZip.h" #include "MMCZip.h"
#include "NullInstance.h" #include "NullInstance.h"
#include "icons/IconList.h" #include "icons/IconList.h"
#include "icons/IconUtils.h" #include "icons/IconUtils.h"
#include "modplatform/technic/TechnicPackProcessor.h"
#include "modplatform/modrinth/ModrinthInstanceCreationTask.h"
#include "modplatform/flame/FlameInstanceCreationTask.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
// FIXME: this does not belong here, it's Minecraft/Flame specific #include <QtConcurrentRun>
#include <quazip/quazipdir.h>
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/PackManifest.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/technic/TechnicPackProcessor.h"
#include "Application.h"
#include "icons/IconList.h"
#include "net/ChecksumValidator.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include <algorithm> #include <algorithm>
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{ {
m_sourceUrl = sourceUrl; m_sourceUrl = sourceUrl;
@ -72,35 +63,41 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
bool InstanceImportTask::abort() bool InstanceImportTask::abort()
{ {
if (!canAbort())
return false;
if (m_filesNetJob) if (m_filesNetJob)
m_filesNetJob->abort(); m_filesNetJob->abort();
m_extractFuture.cancel(); m_extractFuture.cancel();
return false; return Task::abort();
} }
void InstanceImportTask::executeTask() void InstanceImportTask::executeTask()
{ {
if (m_sourceUrl.isLocalFile()) setAbortable(true);
{
if (m_sourceUrl.isLocalFile()) {
m_archivePath = m_sourceUrl.toLocalFile(); m_archivePath = m_sourceUrl.toLocalFile();
processZipPack(); processZipPack();
} } else {
else
{
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
m_downloadRequired = true; m_downloadRequired = true;
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path());
auto entry = APPLICATION->metacache()->resolveEntry("general", path); auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true); entry->setStale(true);
m_archivePath = entry->getFullPath();
m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
auto job = m_filesNetJob.get(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
m_filesNetJob->start(); m_filesNetJob->start();
} }
} }
@ -119,7 +116,13 @@ void InstanceImportTask::downloadFailed(QString reason)
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
{ {
setProgress(current / 2, total); setProgress(current, total);
}
void InstanceImportTask::downloadAborted()
{
emitAborted();
m_filesNetJob.reset();
} }
void InstanceImportTask::processZipPack() void InstanceImportTask::processZipPack()
@ -255,293 +258,31 @@ void InstanceImportTask::extractFinished()
void InstanceImportTask::extractAborted() void InstanceImportTask::extractAborted()
{ {
emitFailed(tr("Instance import has been aborted.")); emitAborted();
return;
} }
void InstanceImportTask::processFlame() void InstanceImportTask::processFlame()
{ {
const static QMap<QString,QString> forgemap = { auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent);
{"1.2.5", "3.4.9.171"},
{"1.4.2", "6.0.1.355"},
{"1.4.7", "6.6.2.534"},
{"1.5.2", "7.8.1.737"}
};
Flame::Manifest pack;
try
{
QString configPath = FS::PathCombine(m_stagingPath, "manifest.json");
Flame::loadManifest(pack, configPath);
QFile::remove(configPath);
}
catch (const JSONValidationError &e)
{
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
return;
}
if(!pack.overrides.isEmpty())
{
QString overridePath = FS::PathCombine(m_stagingPath, pack.overrides);
if (QFile::exists(overridePath))
{
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath))
{
emitFailed(tr("Could not rename the overrides folder:\n") + pack.overrides);
return;
}
}
else
{
logWarning(tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(pack.overrides));
}
}
QString forgeVersion; inst_creation_task->setName(*this);
QString fabricVersion; inst_creation_task->setIcon(m_instIcon);
// TODO: is Quilt relevant here? inst_creation_task->setGroup(m_instGroup);
for(auto &loader: pack.minecraft.modLoaders)
{ connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
auto id = loader.id; setOverride(inst_creation_task->shouldOverride());
if(id.startsWith("forge-")) emitSucceeded();
{
id.remove("forge-");
forgeVersion = id;
continue;
}
if(id.startsWith("fabric-"))
{
id.remove("fabric-");
fabricVersion = id;
continue;
}
logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
auto mcVersion = pack.minecraft.version;
// Hack to correct some 'special sauce'...
if(mcVersion.endsWith('.'))
{
mcVersion.remove(QRegularExpression("[.]+$"));
logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack."));
}
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", mcVersion, true);
if(!forgeVersion.isEmpty())
{
// FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata.
if(forgeVersion == "recommended")
{
if(forgemap.contains(mcVersion))
{
forgeVersion = forgemap[mcVersion];
}
else
{
logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion));
}
}
components->setComponentVersion("net.minecraftforge", forgeVersion);
}
if(!fabricVersion.isEmpty())
{
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
}
if (m_instIcon != "default")
{
instance.setIconKey(m_instIcon);
}
else
{
if(pack.name.contains("Direwolf20"))
{
instance.setIconKey("steve");
}
else if(pack.name.contains("FTB") || pack.name.contains("Feed The Beast"))
{
instance.setIconKey("ftb_logo");
}
else
{
// default to something other than the MultiMC default to distinguish these
instance.setIconKey("flame");
}
}
QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
QFileInfo jarmodsInfo(jarmodsPath);
if(jarmodsInfo.isDir())
{
// install all the jar mods
qDebug() << "Found jarmods:";
QDir jarmodsDir(jarmodsPath);
QStringList jarMods;
for (auto info: jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
{
qDebug() << info.fileName();
jarMods.push_back(info.absoluteFilePath());
}
auto profile = instance.getPackProfile();
profile->installJarMods(jarMods);
// nuke the original files
FS::deletePath(jarmodsPath);
}
instance.setName(m_instName);
m_modIdResolver = new Flame::FileResolvingTask(APPLICATION->network(), pack);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
{
auto results = m_modIdResolver->getResults();
//first check for blocked mods
QString text;
QList<QUrl> urls;
auto anyBlocked = false;
for(const auto& result: results.files.values()) {
if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
urls.append(QUrl(result.websiteUrl));
anyBlocked = true;
}
}
if(anyBlocked) {
qWarning() << "Blocked mods found, displaying mod list";
auto message_dialog = new BlockedModsDialog(m_parent,
tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"),
text,
urls);
message_dialog->setModal(true);
if (message_dialog->exec()) {
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
for (const auto &result: m_modIdResolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
filename += ".disabled";
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
}
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::Download::makeFile(result.url, path);
m_filesNetJob->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
}
}
m_modIdResolver.reset();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
m_filesNetJob.reset();
emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
m_filesNetJob.reset();
emitFailed(reason);
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setProgress(current, total);
});
setStatus(tr("Downloading mods..."));
m_filesNetJob->start();
} else {
m_modIdResolver.reset();
emitFailed("Canceled");
}
} else {
//TODO extract to function ?
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
for (const auto &result: m_modIdResolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
filename += ".disabled";
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
}
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::Download::makeFile(result.url, path);
m_filesNetJob->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
}
}
m_modIdResolver.reset();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
m_filesNetJob.reset();
emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
m_filesNetJob.reset();
emitFailed(reason);
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setProgress(current, total);
});
setStatus(tr("Downloading mods..."));
m_filesNetJob->start();
}
}
);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
{
m_modIdResolver.reset();
emitFailed(tr("Unable to resolve mod IDs:\n") + reason);
}); });
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, [&](qint64 current, qint64 total) connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
{ connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
setProgress(current, total); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
}); connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, [&](QString status)
{ connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
setStatus(status); connect(inst_creation_task, &Task::aborted, this, &Task::abort);
}); connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
m_modIdResolver->start();
inst_creation_task->start();
} }
void InstanceImportTask::processTechnic() void InstanceImportTask::processTechnic()
@ -549,7 +290,7 @@ void InstanceImportTask::processTechnic()
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath);
} }
void InstanceImportTask::processMultiMC() void InstanceImportTask::processMultiMC()
@ -563,7 +304,7 @@ void InstanceImportTask::processMultiMC()
instance.resetTimePlayed(); instance.resetTimePlayed();
// set a new nice name // set a new nice name
instance.setName(m_instName); instance.setName(name());
// if the icon was specified by user, use that. otherwise pull icon from the pack // if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon != "default") { if (m_instIcon != "default") {
@ -584,198 +325,26 @@ void InstanceImportTask::processMultiMC()
emitSucceeded(); emitSucceeded();
} }
// https://docs.modrinth.com/docs/modpacks/format_definition/
void InstanceImportTask::processModrinth() void InstanceImportTask::processModrinth()
{ {
std::vector<Modrinth::File> files; auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString());
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
try {
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
auto doc = Json::requireDocument(indexPath);
auto obj = Json::requireObject(doc, "modrinth.index.json");
int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
if (formatVersion == 1) {
auto game = Json::requireString(obj, "game", "modrinth.index.json");
if (game != "minecraft") {
throw JSONValidationError("Unknown game: " + game);
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); inst_creation_task->setName(*this);
bool had_optional = false; inst_creation_task->setIcon(m_instIcon);
for (auto modInfo : jsonFiles) { inst_creation_task->setGroup(m_instGroup);
Modrinth::File file;
file.path = Json::requireString(modInfo, "path");
auto env = Json::ensureObject(modInfo, "env");
// 'env' field is optional
if (!env.isEmpty()) {
QString support = Json::ensureString(env, "client", "unsupported");
if (support == "unsupported") {
continue;
} else if (support == "optional") {
// TODO: Make a review dialog for choosing which ones the user wants!
if (!had_optional) {
had_optional = true;
auto info = CustomMessageBox::selectable(
m_parent, tr("Optional mod detected!"),
tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"),
QMessageBox::Information);
info->exec();
}
if (file.path.endsWith(".jar"))
file.path += ".disabled";
}
}
QJsonObject hashes = Json::requireObject(modInfo, "hashes");
QString hash;
QCryptographicHash::Algorithm hashAlgorithm;
hash = Json::ensureString(hashes, "sha1");
hashAlgorithm = QCryptographicHash::Sha1;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha512");
hashAlgorithm = QCryptographicHash::Sha512;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha256");
hashAlgorithm = QCryptographicHash::Sha256;
if (hash.isEmpty()) {
throw JSONValidationError("No hash found for: " + file.path);
}
}
}
file.hash = QByteArray::fromHex(hash.toLatin1());
file.hashAlgorithm = hashAlgorithm;
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)
auto download_arr = Json::ensureArray(modInfo, "downloads");
for(auto download : download_arr) {
qWarning() << download.toString();
bool is_last = download.toString() == download_arr.last().toString();
auto download_url = QUrl(download.toString());
if (!download_url.isValid()) {
qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL")
.arg(download_url.toString(), file.path);
if(is_last && file.downloads.isEmpty())
throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
}
else {
file.downloads.push_back(download_url);
}
}
files.push_back(file);
}
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key();
if (name == "minecraft") {
minecraftVersion = Json::requireString(*it, "Minecraft version");
}
else if (name == "fabric-loader") {
fabricVersion = Json::requireString(*it, "Fabric Loader version");
}
else if (name == "quilt-loader") {
quiltVersion = Json::requireString(*it, "Quilt Loader version");
}
else if (name == "forge") {
forgeVersion = Json::requireString(*it, "Forge version");
}
else {
throw JSONValidationError("Unknown dependency type: " + name);
}
}
} else {
throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion));
}
QFile::remove(indexPath);
} catch (const JSONValidationError& e) {
emitFailed(tr("Could not understand pack index:\n") + e.cause());
return;
}
auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
auto override_path = FS::PathCombine(m_stagingPath, "overrides"); emitSucceeded();
if (QFile::exists(override_path)) {
if (!QFile::rename(override_path, mcPath)) {
emitFailed(tr("Could not rename the overrides folder:\n") + "overrides");
return;
}
}
// Do client overrides
auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides");
if (QFile::exists(client_override_path)) {
if (!FS::overrideFolder(mcPath, client_override_path)) {
emitFailed(tr("Could not rename the client overrides folder:\n") + "client overrides");
return;
}
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", minecraftVersion, true);
if (!fabricVersion.isEmpty())
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (!quiltVersion.isEmpty())
components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion);
if (!forgeVersion.isEmpty())
components->setComponentVersion("net.minecraftforge", forgeVersion);
if (m_instIcon != "default")
{
instance.setIconKey(m_instIcon);
}
else
{
instance.setIconKey("modrinth");
}
instance.setName(m_instName);
instance.saveNow();
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
for (auto file : files)
{
auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path);
qDebug() << "Will try to download" << file.downloads.front() << "to" << path;
auto dl = Net::Download::makeFile(file.downloads.dequeue(), path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_filesNetJob->addNetAction(dl);
if (file.downloads.size() > 0) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{
auto dl = Net::Download::makeFile(file.downloads.dequeue(), path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_filesNetJob->addNetAction(dl);
dl->succeeded();
});
}
}
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
{
m_filesNetJob.reset();
emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason)
{
m_filesNetJob.reset();
emitFailed(reason);
}); });
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
{ connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
setProgress(current, total); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
}); connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
setStatus(tr("Downloading mods..."));
m_filesNetJob->start(); connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
connect(inst_creation_task, &Task::aborted, this, &Task::abort);
connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
inst_creation_task->start();
} }

View File

@ -58,7 +58,6 @@ class InstanceImportTask : public InstanceTask
public: public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr);
bool canAbort() const override { return true; }
bool abort() override; bool abort() override;
const QVector<Flame::File> &getBlockedFiles() const const QVector<Flame::File> &getBlockedFiles() const
{ {
@ -80,6 +79,7 @@ private slots:
void downloadSucceeded(); void downloadSucceeded();
void downloadFailed(QString reason); void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total); void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished(); void extractFinished();
void extractAborted(); void extractAborted();

View File

@ -535,7 +535,20 @@ InstancePtr InstanceList::getInstanceById(QString instId) const
return InstancePtr(); return InstancePtr();
} }
QModelIndex InstanceList::getInstanceIndexById(const QString& id) const InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const
{
if (managed_name.isEmpty())
return {};
for (auto instance : m_instances) {
if (instance->getManagedPackName() == managed_name)
return instance;
}
return {};
}
QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
{ {
return index(getInstIndex(getInstanceById(id).get())); return index(getInstIndex(getInstanceById(id).get()));
} }
@ -764,21 +777,17 @@ class InstanceStaging : public Task {
Q_OBJECT Q_OBJECT
const unsigned minBackoff = 1; const unsigned minBackoff = 1;
const unsigned maxBackoff = 16; const unsigned maxBackoff = 16;
public: public:
InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName)
: backoff(minBackoff, maxBackoff) : m_parent(parent), backoff(minBackoff, maxBackoff), m_stagingPath(std::move(stagingPath)), m_instance_name(std::move(instanceName)), m_groupName(std::move(groupName))
{ {
m_parent = parent;
m_child.reset(child); m_child.reset(child);
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded);
connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::status, this, &InstanceStaging::setStatus);
connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::progress, this, &InstanceStaging::setProgress);
m_instanceName = instanceName;
m_groupName = groupName;
m_stagingPath = stagingPath;
m_backoffTimer.setSingleShot(true);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
} }
@ -787,17 +796,16 @@ class InstanceStaging : public Task {
// FIXME/TODO: add ability to abort during instance commit retries // FIXME/TODO: add ability to abort during instance commit retries
bool abort() override bool abort() override
{ {
if (m_child && m_child->canAbort()) { if (!canAbort())
return m_child->abort(); return false;
}
return false; m_child->abort();
return Task::abort();
} }
bool canAbort() const override bool canAbort() const override
{ {
if (m_child && m_child->canAbort()) { return (m_child && m_child->canAbort());
return true;
}
return false;
} }
protected: protected:
@ -808,7 +816,8 @@ class InstanceStaging : public Task {
void childSucceded() void childSucceded()
{ {
unsigned sleepTime = backoff(); unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) { if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride()))
{
emitSucceeded(); emitSucceeded();
return; return;
} }
@ -817,7 +826,7 @@ class InstanceStaging : public Task {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return; return;
} }
qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500); m_backoffTimer.start(sleepTime * 500);
} }
void childFailed(const QString& reason) void childFailed(const QString& reason)
@ -826,7 +835,13 @@ class InstanceStaging : public Task {
emitFailed(reason); emitFailed(reason);
} }
private: void childAborted()
{
emitAborted();
}
private:
InstanceList * m_parent;
/* /*
* WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
* Basically, it starts messing things up while the launcher is extracting/creating instances * Basically, it starts messing things up while the launcher is extracting/creating instances
@ -834,9 +849,8 @@ class InstanceStaging : public Task {
*/ */
ExponentialSeries backoff; ExponentialSeries backoff;
QString m_stagingPath; QString m_stagingPath;
InstanceList* m_parent; unique_qobject_ptr<InstanceTask> m_child;
unique_qobject_ptr<Task> m_child; InstanceName m_instance_name;
QString m_instanceName;
QString m_groupName; QString m_groupName;
QTimer m_backoffTimer; QTimer m_backoffTimer;
}; };
@ -846,7 +860,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task)
auto stagingPath = getStagedInstancePath(); auto stagingPath = getStagedInstancePath();
task->setStagingPath(stagingPath); task->setStagingPath(stagingPath);
task->setParentSettings(m_globalSettings); task->setParentSettings(m_globalSettings);
return new InstanceStaging(this, task, stagingPath, task->name(), task->group()); return new InstanceStaging(this, task, stagingPath, *task, task->group());
} }
QString InstanceList::getStagedInstancePath() QString InstanceList::getStagedInstancePath()
@ -866,23 +880,50 @@ QString InstanceList::getStagedInstancePath()
return path; return path;
} }
bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName) bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override)
{ {
QDir dir; QDir dir;
QString instID = FS::DirNameFromString(instanceName, m_instDir); QString instID;
InstancePtr inst;
if (should_override) {
// This is to avoid problems when the instance folder gets manually renamed
if ((inst = getInstanceByManagedName(instanceName.originalName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else {
instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-');
}
} else {
instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir);
}
{ {
WatchLock lock(m_watcher, m_instDir); WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID); QString destination = FS::PathCombine(m_instDir, instID);
if (!dir.rename(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination; if (should_override) {
return false; if (!FS::overrideFolder(destination, path)) {
qWarning() << "Failed to override" << path << "to" << destination;
return false;
}
} else {
if (!dir.rename(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination;
return false;
}
m_instanceGroupIndex[instID] = groupName;
m_groupNameCache.insert(groupName);
} }
m_instanceGroupIndex[instID] = groupName;
instanceSet.insert(instID); instanceSet.insert(instID);
m_groupNameCache.insert(groupName);
emit instancesChanged(); emit instancesChanged();
emit instanceSelectRequest(instID); emit instanceSelectRequest(instID);
} }
saveGroupList(); saveGroupList();
return true; return true;
} }

View File

@ -24,10 +24,10 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "QObjectPtr.h"
class QFileSystemWatcher; class QFileSystemWatcher;
class InstanceTask; class InstanceTask;
struct InstanceName;
using InstanceId = QString; using InstanceId = QString;
using GroupId = QString; using GroupId = QString;
using InstanceLocator = std::pair<InstancePtr, int>; using InstanceLocator = std::pair<InstancePtr, int>;
@ -101,7 +101,10 @@ public:
InstListError loadList(); InstListError loadList();
void saveNow(); void saveNow();
/* O(n) */
InstancePtr getInstanceById(QString id) const; InstancePtr getInstanceById(QString id) const;
/* O(n) */
InstancePtr getInstanceByManagedName(const QString& managed_name) const;
QModelIndex getInstanceIndexById(const QString &id) const; QModelIndex getInstanceIndexById(const QString &id) const;
QStringList getGroups(); QStringList getGroups();
bool isGroupCollapsed(const QString &groupName); bool isGroupCollapsed(const QString &groupName);
@ -127,8 +130,10 @@ public:
/** /**
* Commit the staging area given by @keyPath to the provider - used when creation succeeds. * Commit the staging area given by @keyPath to the provider - used when creation succeeds.
* Used by instance manipulation tasks. * Used by instance manipulation tasks.
* should_override is used when another similar instance already exists, and we want to override it
* - for instance, when updating it.
*/ */
bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName); bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override);
/** /**
* Destroy a previously created staging area given by @keyPath - used when creation fails. * Destroy a previously created staging area given by @keyPath - used when creation fails.

View File

@ -1,9 +1,52 @@
#include "InstanceTask.h" #include "InstanceTask.h"
InstanceTask::InstanceTask() #include "ui/dialogs/CustomMessageBox.h"
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{ {
auto dialog =
CustomMessageBox::selectable(parent, QObject::tr("Change instance name"),
QObject::tr("The instance's name seems to include the old version. Would you like to update it?\n\n"
"Old name: %1\n"
"New name: %2")
.arg(old_name, new_name),
QMessageBox::Question, QMessageBox::No | QMessageBox::Yes);
auto result = dialog->exec();
if (result == QMessageBox::Yes)
return InstanceNameChange::ShouldChange;
return InstanceNameChange::ShouldKeep;
} }
InstanceTask::~InstanceTask() QString InstanceName::name() const
{ {
if (!m_modified_name.isEmpty())
return modifiedName();
return QString("%1 %2").arg(m_original_name, m_original_version);
} }
QString InstanceName::originalName() const
{
return m_original_name;
}
QString InstanceName::modifiedName() const
{
if (!m_modified_name.isEmpty())
return m_modified_name;
return m_original_name;
}
QString InstanceName::version() const
{
return m_original_version;
}
void InstanceName::setName(InstanceName& other)
{
m_original_name = other.m_original_name;
m_original_version = other.m_original_version;
m_modified_name = other.m_modified_name;
}
InstanceTask::InstanceTask() : Task(), InstanceName() {}

View File

@ -1,52 +1,57 @@
#pragma once #pragma once
#include "tasks/Task.h"
#include "settings/SettingsObject.h" #include "settings/SettingsObject.h"
#include "tasks/Task.h"
class InstanceTask : public Task /* Helpers */
{ enum class InstanceNameChange { ShouldChange, ShouldKeep };
[[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name);
struct InstanceName {
public:
InstanceName() = default;
InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {}
[[nodiscard]] QString modifiedName() const;
[[nodiscard]] QString originalName() const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString version() const;
void setName(QString name) { m_modified_name = name; }
void setName(InstanceName& other);
protected:
QString m_original_name;
QString m_original_version;
QString m_modified_name;
};
class InstanceTask : public Task, public InstanceName {
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceTask(); InstanceTask();
virtual ~InstanceTask(); ~InstanceTask() override = default;
void setParentSettings(SettingsObjectPtr settings) void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; }
{
m_globalSettings = settings;
}
void setStagingPath(const QString &stagingPath) void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; }
{
m_stagingPath = stagingPath;
}
void setName(const QString &name) void setIcon(const QString& icon) { m_instIcon = icon; }
{
m_instName = name;
}
QString name() const
{
return m_instName;
}
void setIcon(const QString &icon) void setGroup(const QString& group) { m_instGroup = group; }
{ QString group() const { return m_instGroup; }
m_instIcon = icon;
}
void setGroup(const QString &group) bool shouldOverride() const { return m_override_existing; }
{
m_instGroup = group;
}
QString group() const
{
return m_instGroup;
}
protected: /* data */ protected:
void setOverride(bool override) { m_override_existing = override; }
protected: /* data */
SettingsObjectPtr m_globalSettings; SettingsObjectPtr m_globalSettings;
QString m_instName;
QString m_instIcon; QString m_instIcon;
QString m_instGroup; QString m_instGroup;
QString m_stagingPath; QString m_stagingPath;
bool m_override_existing = false;
}; };

View File

@ -167,6 +167,7 @@ void LaunchController::login() {
tries++; tries++;
m_session = std::make_shared<AuthSession>(); m_session = std::make_shared<AuthSession>();
m_session->wants_online = m_online; m_session->wants_online = m_online;
m_session->demo = m_demo;
m_accountToUse->fillSession(m_session); m_accountToUse->fillSession(m_session);
// Launch immediately in true offline mode // Launch immediately in true offline mode
@ -184,12 +185,18 @@ void LaunchController::login() {
if(!m_session->wants_online) { if(!m_session->wants_online) {
// we ask the user for a player name // we ask the user for a player name
bool ok = false; bool ok = false;
QString message = tr("Choose your offline mode player name.");
if(m_session->demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName; QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName;
QString name = QInputDialog::getText( QString name = QInputDialog::getText(
m_parentWidget, m_parentWidget,
tr("Player name"), tr("Player name"),
tr("Choose your offline mode player name."), message,
QLineEdit::Normal, QLineEdit::Normal,
usedname, usedname,
&ok &ok
@ -369,7 +376,7 @@ void LaunchController::launchInstance()
} }
m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} else { } else {
online_mode = "offline"; online_mode = m_demo ? "demo" : "offline";
} }
m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));

View File

@ -63,6 +63,10 @@ public:
m_online = online; m_online = online;
} }
void setDemo(bool demo) {
m_demo = demo;
}
void setProfiler(BaseProfilerFactory *profiler) { void setProfiler(BaseProfilerFactory *profiler) {
m_profiler = profiler; m_profiler = profiler;
} }
@ -101,6 +105,7 @@ private slots:
private: private:
BaseProfilerFactory *m_profiler = nullptr; BaseProfilerFactory *m_profiler = nullptr;
bool m_online = true; bool m_online = true;
bool m_demo = false;
InstancePtr m_instance; InstancePtr m_instance;
QWidget * m_parentWidget = nullptr; QWidget * m_parentWidget = nullptr;
InstanceWindow *m_console = nullptr; InstanceWindow *m_console = nullptr;

View File

@ -245,6 +245,14 @@ QString MinecraftInstance::getLocalLibraryPath() const
return libraries_dir.absolutePath(); return libraries_dir.absolutePath();
} }
bool MinecraftInstance::supportsDemo() const
{
Version instance_ver { getPackProfile()->getComponentVersion("net.minecraft") };
// Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History
// FIXME: Due to Version constraints atm, this can't handle well non-release versions
return instance_ver >= Version("1.3.1");
}
QString MinecraftInstance::jarModsDir() const QString MinecraftInstance::jarModsDir() const
{ {
QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/"));
@ -714,7 +722,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{ {
out << QString("%1:").arg(label); out << QString("%1:").arg(label);
auto modList = model.allMods(); auto modList = model.allMods();
std::sort(modList.begin(), modList.end(), [](Mod::Ptr a, Mod::Ptr b) { std::sort(modList.begin(), modList.end(), [](auto a, auto b) {
auto aName = a->fileinfo().completeBaseName(); auto aName = a->fileinfo().completeBaseName();
auto bName = b->fileinfo().completeBaseName(); auto bName = b->fileinfo().completeBaseName();
return aName.localeAwareCompare(bName) < 0; return aName.localeAwareCompare(bName) < 0;

View File

@ -70,6 +70,8 @@ public:
// where the instance-local libraries should be // where the instance-local libraries should be
QString getLocalLibraryPath() const; QString getLocalLibraryPath() const;
/** Returns whether the instance, with its version, has support for demo mode. */
[[nodiscard]] bool supportsDemo() const;
////// Profile management ////// ////// Profile management //////
std::shared_ptr<PackProfile> getPackProfile() const; std::shared_ptr<PackProfile> getPackProfile() const;

View File

@ -0,0 +1,34 @@
#include "VanillaInstanceCreationTask.h"
#include <utility>
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "settings/INISettingsObject.h"
VanillaCreationTask::VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version)
: InstanceCreationTask(), m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loader_version))
{}
bool VanillaCreationTask::createInstance()
{
setStatus(tr("Creating instance from version %1").arg(m_version->name()));
auto instance_settings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
instance_settings->suspendSave();
{
MinecraftInstance inst(m_globalSettings, instance_settings, m_stagingPath);
auto components = inst.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
if(m_using_loader)
components->setComponentVersion(m_loader, m_loader_version->descriptor());
inst.setName(name());
inst.setIconKey(m_instIcon);
}
instance_settings->resumeSave();
return true;
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "InstanceCreationTask.h"
#include <utility>
class VanillaCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
VanillaCreationTask(BaseVersionPtr version) : InstanceCreationTask(), m_version(std::move(version)) {}
VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version);
bool createInstance() override;
private:
// Version to update to / create of the instance.
BaseVersionPtr m_version;
bool m_using_loader = false;
QString m_loader;
BaseVersionPtr m_loader_version;
};

View File

@ -51,7 +51,6 @@
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed) ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
{ {
FS::ensureFolderPathExists(m_dir.absolutePath());
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE }; m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
} }
@ -234,7 +233,7 @@ auto ModFolderModel::allMods() -> QList<Mod*>
{ {
QList<Mod*> mods; QList<Mod*> mods;
for (auto& res : m_resources) { for (auto& res : qAsConst(m_resources)) {
mods.append(static_cast<Mod*>(res.get())); mods.append(static_cast<Mod*>(res.get()));
} }

View File

@ -14,6 +14,8 @@
ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this) ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
{ {
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
@ -251,7 +253,7 @@ bool ResourceFolderModel::update()
return true; return true;
} }
void ResourceFolderModel::resolveResource(Resource::Ptr res) void ResourceFolderModel::resolveResource(Resource* res)
{ {
if (!res->shouldResolve()) { if (!res->shouldResolve()) {
return; return;

View File

@ -68,7 +68,7 @@ class ResourceFolderModel : public QAbstractListModel {
virtual bool update(); virtual bool update();
/** Creates a new parse task, if needed, for 'res' and start it.*/ /** Creates a new parse task, if needed, for 'res' and start it.*/
virtual void resolveResource(Resource::Ptr res); virtual void resolveResource(Resource* res);
[[nodiscard]] size_t size() const { return m_resources.size(); }; [[nodiscard]] size_t size() const { return m_resources.size(); };
[[nodiscard]] bool empty() const { return size() == 0; } [[nodiscard]] bool empty() const { return size() == 0; }
@ -247,7 +247,7 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
auto row = row_it.value(); auto row = row_it.value();
auto& new_resource = new_resources[kept]; auto& new_resource = new_resources[kept];
auto const& current_resource = m_resources[row]; auto const& current_resource = m_resources.at(row);
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
// no significant change, ignore... // no significant change, ignore...
@ -265,7 +265,7 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
} }
m_resources[row].reset(new_resource); m_resources[row].reset(new_resource);
resolveResource(m_resources.at(row)); resolveResource(m_resources.at(row).get());
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
} }
} }
@ -313,7 +313,7 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
for (auto& added : added_set) { for (auto& added : added_set) {
auto res = new_resources[added]; auto res = new_resources[added];
m_resources.append(res); m_resources.append(res);
resolveResource(res); resolveResource(m_resources.last().get());
} }
endInsertRows(); endInsertRows();
@ -324,7 +324,7 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
{ {
m_resources_index.clear(); m_resources_index.clear();
int idx = 0; int idx = 0;
for (auto const& mod : m_resources) { for (auto const& mod : qAsConst(m_resources)) {
m_resources_index[mod->internal_id()] = idx; m_resources_index[mod->internal_id()] = idx;
idx++; idx++;
} }

View File

@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "TexturePack.h"
#include <QDebug>
#include <QMap>
#include <QRegularExpression>
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
void TexturePack::setDescription(QString new_description)
{
QMutexLocker locker(&m_data_lock);
m_description = new_description;
}
void TexturePack::setImage(QImage new_image)
{
QMutexLocker locker(&m_data_lock);
Q_ASSERT(!new_image.isNull());
if (m_pack_image_cache_key.key.isValid())
QPixmapCache::remove(m_pack_image_cache_key.key);
m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image));
m_pack_image_cache_key.was_ever_used = true;
}
QPixmap TexturePack::image(QSize size)
{
QPixmap cached_image;
if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
return cached_image.scaled(size);
}
// No valid image we can get
if (!m_pack_image_cache_key.was_ever_used)
return {};
// Imaged got evicted from the cache. Re-process it and retry.
TexturePackUtils::process(*this);
return image(size);
}

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "Resource.h"
#include <QImage>
#include <QMutex>
#include <QPixmap>
#include <QPixmapCache>
class Version;
class TexturePack : public Resource {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Resource>;
TexturePack(QObject* parent = nullptr) : Resource(parent) {}
TexturePack(QFileInfo file_info) : Resource(file_info) {}
/** Gets the description of the texture pack. */
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size);
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */
void setImage(QImage new_image);
protected:
mutable QMutex m_data_lock;
/** The texture pack's description, as defined in the pack.txt file.
*/
QString m_description;
/** The texture pack's image file cache key, for access in the QPixmapCache global instance.
*
* The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true),
* so as to tell whether a cache entry is inexistent or if it was just evicted from the cache.
*/
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
} m_pack_image_cache_key;
};

View File

@ -1,38 +1,52 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by * This program is free software: you can redistribute it and/or modify
* the Free Software Foundation, version 3. * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, version 3.
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of * This program is distributed in the hope that it will be useful,
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * but WITHOUT ANY WARRANTY; without even the implied warranty of
* GNU General Public License for more details. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <https://www.gnu.org/licenses/>. * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
* This file incorporates work covered by the following copyright and *
* permission notice: * This file incorporates work covered by the following copyright and
* * permission notice:
* Copyright 2013-2021 MultiMC Contributors *
* * Copyright 2013-2021 MultiMC Contributors
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. * Licensed under the Apache License, Version 2.0 (the "License");
* You may obtain a copy of the License at * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0 *
* * http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, * Unless required by applicable law or agreed to in writing, software
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * distributed under the License is distributed on an "AS IS" BASIS,
* See the License for the specific language governing permissions and * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* limitations under the License. * See the License for the specific language governing permissions and
*/ * limitations under the License.
*/
#include "TexturePackFolderModel.h" #include "TexturePackFolderModel.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
Task* TexturePackFolderModel::createUpdateTask()
{
return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new TexturePack(entry); });
}
Task* TexturePackFolderModel::createParseTask(Resource& resource)
{
return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource));
}

View File

@ -1,3 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once #pragma once
#include "ResourceFolderModel.h" #include "ResourceFolderModel.h"
@ -8,4 +44,6 @@ class TexturePackFolderModel : public ResourceFolderModel
public: public:
explicit TexturePackFolderModel(const QString &dir); explicit TexturePackFolderModel(const QString &dir);
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource&) override;
}; };

View File

@ -3,6 +3,7 @@
#include <QDir> #include <QDir>
#include <QMap> #include <QMap>
#include <QObject> #include <QObject>
#include <QThread>
#include <memory> #include <memory>
@ -23,14 +24,14 @@ class BasicFolderLoadTask : public Task {
[[nodiscard]] ResultPtr result() const { return m_result; } [[nodiscard]] ResultPtr result() const { return m_result; }
public: public:
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread())
{ {
m_create_func = [](QFileInfo const& entry) -> Resource* { m_create_func = [](QFileInfo const& entry) -> Resource* {
return new Resource(entry); return new Resource(entry);
}; };
} }
BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function) BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function)
: Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)), m_thread_to_spawn_into(thread())
{} {}
[[nodiscard]] bool canAbort() const override { return true; } [[nodiscard]] bool canAbort() const override { return true; }
@ -42,9 +43,13 @@ class BasicFolderLoadTask : public Task {
void executeTask() override void executeTask() override
{ {
if (thread() != m_thread_to_spawn_into)
connect(this, &Task::finished, this->thread(), &QThread::quit);
m_dir.refresh(); m_dir.refresh();
for (auto entry : m_dir.entryInfoList()) { for (auto entry : m_dir.entryInfoList()) {
auto resource = m_create_func(entry); auto resource = m_create_func(entry);
resource->moveToThread(m_thread_to_spawn_into);
m_result->resources.insert(resource->internal_id(), resource); m_result->resources.insert(resource->internal_id(), resource);
} }
@ -61,4 +66,7 @@ private:
std::atomic<bool> m_aborted = false; std::atomic<bool> m_aborted = false;
std::function<Resource*(QFileInfo const&)> m_create_func; std::function<Resource*(QFileInfo const&)> m_create_func;
/** This is the thread in which we should put new mod objects */
QThread* m_thread_to_spawn_into;
}; };

View File

@ -0,0 +1,155 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "LocalTexturePackParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include <QCryptographicHash>
namespace TexturePackUtils {
bool process(TexturePack& pack)
{
switch (pack.type()) {
case ResourceType::FOLDER:
TexturePackUtils::processFolder(pack);
return true;
case ResourceType::ZIPFILE:
TexturePackUtils::processZIP(pack);
return true;
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
void processFolder(TexturePack& pack)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.txt"));
if (mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
return;
auto data = mcmeta_file.readAll();
TexturePackUtils::processPackTXT(pack, std::move(data));
mcmeta_file.close();
}
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.isFile()) {
QFile mcmeta_file(image_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
return;
auto data = mcmeta_file.readAll();
TexturePackUtils::processPackPNG(pack, std::move(data));
mcmeta_file.close();
}
}
void processZIP(TexturePack& pack)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return;
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.txt")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return;
}
auto data = file.readAll();
TexturePackUtils::processPackTXT(pack, std::move(data));
file.close();
}
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return;
}
auto data = file.readAll();
TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
}
zip.close();
}
void processPackTXT(TexturePack& pack, QByteArray&& raw_data)
{
pack.setDescription(QString(raw_data));
}
void processPackPNG(TexturePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
pack.setImage(img);
} else {
qWarning() << "Failed to parse pack.png.";
}
}
} // namespace TexturePackUtils
LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp)
: Task(nullptr, false), m_token(token), m_texture_pack(rp)
{}
bool LocalTexturePackParseTask::abort()
{
m_aborted = true;
return true;
}
void LocalTexturePackParseTask::executeTask()
{
Q_ASSERT(m_texture_pack.valid());
if (!TexturePackUtils::process(m_texture_pack))
return;
if (m_aborted)
emitAborted();
else
emitSucceeded();
}

View File

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDebug>
#include <QObject>
#include "minecraft/mod/TexturePack.h"
#include "tasks/Task.h"
namespace TexturePackUtils {
bool process(TexturePack& pack);
void processZIP(TexturePack& pack);
void processFolder(TexturePack& pack);
void processPackTXT(TexturePack& pack, QByteArray&& raw_data);
void processPackPNG(TexturePack& pack, QByteArray&& raw_data);
} // namespace TexturePackUtils
class LocalTexturePackParseTask : public Task {
Q_OBJECT
public:
LocalTexturePackParseTask(int token, TexturePack& rp);
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
void executeTask() override;
[[nodiscard]] int token() const { return m_token; }
private:
int m_token;
TexturePack& m_texture_pack;
bool m_aborted = false;
};

View File

@ -38,12 +38,23 @@
#include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/MetadataHandler.h"
#include <QThread>
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan)
: Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) : Task(nullptr, false)
, m_mods_dir(mods_dir)
, m_index_dir(index_dir)
, m_is_indexed(is_indexed)
, m_clean_orphan(clean_orphan)
, m_result(new Result())
, m_thread_to_spawn_into(thread())
{} {}
void ModFolderLoadTask::executeTask() void ModFolderLoadTask::executeTask()
{ {
if (thread() != m_thread_to_spawn_into)
connect(this, &Task::finished, this->thread(), &QThread::quit);
if (m_is_indexed) { if (m_is_indexed) {
// Read metadata first // Read metadata first
getFromMetadata(); getFromMetadata();
@ -57,6 +68,8 @@ void ModFolderLoadTask::executeTask()
if (mod->enabled()) { if (mod->enabled()) {
if (m_result->mods.contains(mod->internal_id())) { if (m_result->mods.contains(mod->internal_id())) {
m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed);
// Delete the object we just created, since a valid one is already in the mods list.
delete mod;
} }
else { else {
m_result->mods[mod->internal_id()] = mod; m_result->mods[mod->internal_id()] = mod;
@ -86,7 +99,7 @@ void ModFolderLoadTask::executeTask()
// Remove orphan metadata to prevent issues // Remove orphan metadata to prevent issues
// See https://github.com/PolyMC/PolyMC/issues/996 // See https://github.com/PolyMC/PolyMC/issues/996
if (m_clean_orphan) { if (m_clean_orphan) {
QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods); QMutableMapIterator iter(m_result->mods);
while (iter.hasNext()) { while (iter.hasNext()) {
auto mod = iter.next().value(); auto mod = iter.next().value();
if (mod->status() == ModStatus::NotInstalled) { if (mod->status() == ModStatus::NotInstalled) {
@ -96,6 +109,9 @@ void ModFolderLoadTask::executeTask()
} }
} }
for (auto mod : m_result->mods)
mod->moveToThread(m_thread_to_spawn_into);
if (m_aborted) if (m_aborted)
emit finished(); emit finished();
else else

View File

@ -79,4 +79,7 @@ private:
ResultPtr m_result; ResultPtr m_result;
std::atomic<bool> m_aborted = false; std::atomic<bool> m_aborted = false;
/** This is the thread in which we should put new mod objects */
QThread* m_thread_to_spawn_into;
}; };

View File

@ -90,6 +90,7 @@ void PackInstallTask::executeTask()
QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
QObject::connect(netJob, &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
} }
void PackInstallTask::onDownloadSucceeded() void PackInstallTask::onDownloadSucceeded()
@ -169,6 +170,12 @@ void PackInstallTask::onDownloadFailed(QString reason)
emitFailed(reason); emitFailed(reason);
} }
void PackInstallTask::onDownloadAborted()
{
jobPtr.reset();
emitAborted();
}
void PackInstallTask::deleteExistingFiles() void PackInstallTask::deleteExistingFiles()
{ {
setStatus(tr("Deleting existing files...")); setStatus(tr("Deleting existing files..."));
@ -675,6 +682,11 @@ void PackInstallTask::installConfigs()
abortable = true; abortable = true;
setProgress(current, total); setProgress(current, total);
}); });
connect(jobPtr.get(), &NetJob::aborted, [&]{
abortable = false;
jobPtr.reset();
emitAborted();
});
jobPtr->start(); jobPtr->start();
} }
@ -831,6 +843,12 @@ void PackInstallTask::downloadMods()
abortable = true; abortable = true;
setProgress(current, total); setProgress(current, total);
}); });
connect(jobPtr.get(), &NetJob::aborted, [&]
{
abortable = false;
jobPtr.reset();
emitAborted();
});
jobPtr->start(); jobPtr->start();
} }
@ -1005,7 +1023,7 @@ void PackInstallTask::install()
components->saveNow(); components->saveNow();
instance.setName(m_instName); instance.setName(name());
instance.setIconKey(m_instIcon); instance.setIconKey(m_instIcon);
instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name);
instanceSettings->resumeSave(); instanceSettings->resumeSave();

View File

@ -93,6 +93,7 @@ protected:
private slots: private slots:
void onDownloadSucceeded(); void onDownloadSucceeded();
void onDownloadFailed(QString reason); void onDownloadFailed(QString reason);
void onDownloadAborted();
void onModsDownloaded(); void onModsDownloaded();
void onModsExtracted(); void onModsExtracted();

View File

@ -183,3 +183,26 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const ->
return netJob; return netJob;
} }
auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*
{
auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network());
QJsonObject body_obj;
QJsonArray files_arr;
for (auto& fileId : fileIds) {
files_arr.append(fileId);
}
body_obj["fileIds"] = files_arr;
QJsonDocument body(body_obj);
auto body_raw = body.toJson();
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), 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;
}

View File

@ -12,6 +12,7 @@ class FlameAPI : public NetworkModAPI {
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*;
private: private:
inline auto getSortFieldInt(QString sortString) const -> int inline auto getSortFieldInt(QString sortString) const -> int

View File

@ -0,0 +1,457 @@
#include "FlameInstanceCreationTask.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/PackManifest.h"
#include "Application.h"
#include "FileSystem.h"
#include "InstanceList.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/helpers/OverrideUtils.h"
#include "settings/INISettingsObject.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
const static QMap<QString, QString> forgemap = { { "1.2.5", "3.4.9.171" },
{ "1.4.2", "6.0.1.355" },
{ "1.4.7", "6.6.2.534" },
{ "1.5.2", "7.8.1.737" } };
static const FlameAPI api;
bool FlameCreationTask::abort()
{
if (!canAbort())
return false;
m_abort = true;
if (m_process_update_file_info_job)
m_process_update_file_info_job->abort();
if (m_files_job)
m_files_job->abort();
if (m_mod_id_resolver)
m_mod_id_resolver->abort();
return Task::abort();
}
bool FlameCreationTask::updateInstance()
{
auto instance_list = APPLICATION->instances();
// FIXME: How to handle situations when there's more than one install already for a given modpack?
auto inst = instance_list->getInstanceByManagedName(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
if (!inst)
return false;
}
QString index_path(FS::PathCombine(m_stagingPath, "manifest.json"));
try {
Flame::loadManifest(m_pack, index_path);
} catch (const JSONValidationError& e) {
setError(tr("Could not understand pack manifest:\n") + e.cause());
return false;
}
auto version_id = inst->getManagedPackVersionName();
auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : "";
auto info = CustomMessageBox::selectable(
m_parent, tr("Similar modpack was found!"),
tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(version_str), QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, tr("Update existing instance"));
info->setButtonText(QMessageBox::Abort, tr("Create new instance"));
info->setButtonText(QMessageBox::Reset, tr("Cancel"));
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Abort))
return false;
if (info->clickedButton() == info->button(QMessageBox::Reset)) {
m_abort = true;
return false;
}
QDir old_inst_dir(inst->instanceRoot());
QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "flame"));
QString old_index_path(FS::PathCombine(old_index_folder, "manifest.json"));
QFileInfo old_index_file(old_index_path);
if (old_index_file.exists()) {
Flame::Manifest old_pack;
Flame::loadManifest(old_pack, old_index_path);
auto& old_files = old_pack.files;
auto& files = m_pack.files;
// Remove repeated files, we don't need to download them!
auto files_iterator = files.begin();
while (files_iterator != files.end()) {
auto const& file = files_iterator;
auto old_file = old_files.find(file.key());
if (old_file != old_files.end()) {
// We found a match, but is it a different version?
if (old_file->fileId == file->fileId) {
qDebug() << "Removed file at" << file->targetFolder << "with id" << file->fileId << "from list of downloads";
old_files.remove(file.key());
files_iterator = files.erase(files_iterator);
}
}
files_iterator++;
}
QDir old_minecraft_dir(inst->gameRoot());
// We will remove all the previous overrides, to prevent duplicate files!
// TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides?
// FIXME: We may want to do something about disabled mods.
auto old_overrides = Override::readOverrides("overrides", old_index_folder);
for (const auto& entry : old_overrides) {
if (entry.isEmpty())
continue;
qDebug() << "Scheduling" << entry << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry));
}
// Remove remaining old files (we need to do an API request to know which ids are which files...)
QStringList fileIds;
for (auto& file : old_files) {
fileIds.append(QString::number(file.fileId));
}
auto* raw_response = new QByteArray;
auto job = api.getFiles(fileIds, raw_response);
QEventLoop loop;
connect(job, &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] {
// Parse the API response
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*raw_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame files task at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *raw_response;
return;
}
try {
QJsonArray entries;
if (fileIds.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);
Flame::File file;
// We don't care about blocked mods, we just need local data to delete the file
file.parseFromObject(entry_obj, false);
auto id = Json::requireInteger(entry_obj, "id");
old_files.insert(id, file);
}
} catch (Json::JsonException& e) {
qCritical() << e.cause() << e.what();
}
// Delete the files
for (auto& file : old_files) {
if (file.fileName.isEmpty() || file.targetFolder.isEmpty())
continue;
QString relative_path(FS::PathCombine(file.targetFolder, file.fileName));
qDebug() << "Scheduling" << relative_path << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
}
});
connect(job, &NetJob::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = job;
job->start();
loop.exec();
m_process_update_file_info_job = nullptr;
} else {
// We don't have an old index file, so we may duplicate stuff!
auto dialog = CustomMessageBox::selectable(m_parent,
tr("No index file."),
tr("We couldn't find a suitable index file for the older version. This may cause some of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
if (dialog->exec() == QDialog::DialogCode::Rejected) {
m_abort = true;
return false;
}
}
setOverride(true);
qDebug() << "Will override instance!";
m_instance = inst;
// We let it go through the createInstance() stage, just with a couple modifications for updating
return false;
}
bool FlameCreationTask::createInstance()
{
QEventLoop loop;
QString parent_folder(FS::PathCombine(m_stagingPath, "flame"));
try {
QString index_path(FS::PathCombine(m_stagingPath, "manifest.json"));
if (!m_pack.is_loaded)
Flame::loadManifest(m_pack, index_path);
// Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "manifest.json"));
FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place);
} catch (const JSONValidationError& e) {
setError(tr("Could not understand pack manifest:\n") + e.cause());
return false;
}
if (!m_pack.overrides.isEmpty()) {
QString overridePath = FS::PathCombine(m_stagingPath, m_pack.overrides);
if (QFile::exists(overridePath)) {
// Create a list of overrides in "overrides.txt" inside flame/
Override::createOverrides("overrides", parent_folder, overridePath);
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides);
return false;
}
} else {
logWarning(
tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(m_pack.overrides));
}
}
QString forgeVersion;
QString fabricVersion;
// TODO: is Quilt relevant here?
for (auto& loader : m_pack.minecraft.modLoaders) {
auto id = loader.id;
if (id.startsWith("forge-")) {
id.remove("forge-");
forgeVersion = id;
continue;
}
if (id.startsWith("fabric-")) {
id.remove("fabric-");
fabricVersion = id;
continue;
}
logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
auto mcVersion = m_pack.minecraft.version;
// Hack to correct some 'special sauce'...
if (mcVersion.endsWith('.')) {
mcVersion.remove(QRegularExpression("[.]+$"));
logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack."));
}
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", mcVersion, true);
if (!forgeVersion.isEmpty()) {
// FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata.
if (forgeVersion == "recommended") {
if (forgemap.contains(mcVersion)) {
forgeVersion = forgemap[mcVersion];
} else {
logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion));
}
}
components->setComponentVersion("net.minecraftforge", forgeVersion);
}
if (!fabricVersion.isEmpty())
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
} else {
if (m_pack.name.contains("Direwolf20")) {
instance.setIconKey("steve");
} else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast")) {
instance.setIconKey("ftb_logo");
} else {
instance.setIconKey("flame");
}
}
QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
QFileInfo jarmodsInfo(jarmodsPath);
if (jarmodsInfo.isDir()) {
// install all the jar mods
qDebug() << "Found jarmods:";
QDir jarmodsDir(jarmodsPath);
QStringList jarMods;
for (const auto& info : jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
qDebug() << info.fileName();
jarMods.push_back(info.absoluteFilePath());
}
auto profile = instance.getPackProfile();
profile->installJarMods(jarMods);
// nuke the original files
FS::deletePath(jarmodsPath);
}
instance.setManagedPack("flame", {}, m_pack.name, {}, m_pack.version);
instance.setName(name());
m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) {
m_mod_id_resolver.reset();
setError(tr("Unable to resolve mod IDs:\n") + reason);
});
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
m_mod_id_resolver->start();
loop.exec();
bool did_succeed = getError().isEmpty();
// Update information of the already installed instance, if any.
if (m_instance && did_succeed) {
setAbortable(false);
auto inst = m_instance.value();
// Only change the name if it didn't use a custom name, so that the previous custom name
// is preserved, but if we're using the original one, we update the version string.
// NOTE: This needs to come before the copyManagedPack call!
if (inst->name().contains(inst->getManagedPackVersionName())) {
if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange)
inst->setName(instance.name());
}
inst->copyManagedPack(instance);
}
return did_succeed;
}
void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
{
auto results = m_mod_id_resolver->getResults();
// first check for blocked mods
QString text;
QList<QUrl> urls;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
urls.append(QUrl(result.websiteUrl));
anyBlocked = true;
}
}
if (anyBlocked) {
qWarning() << "Blocked mods found, displaying mod list";
auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"),
text,
urls);
message_dialog->setModal(true);
if (message_dialog->exec()) {
setupDownloadJob(loop);
} else {
m_mod_id_resolver.reset();
setError("Canceled");
loop.quit();
}
} else {
setupDownloadJob(loop);
}
}
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
for (const auto& result : m_mod_id_resolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
filename += ".disabled";
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
}
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::Download::makeFile(result.url, path);
m_files_job->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
}
}
m_mod_id_resolver.reset();
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
m_files_job.reset();
});
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset();
setError(reason);
});
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); });
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));
m_files_job->start();
}

View File

@ -0,0 +1,44 @@
#pragma once
#include "InstanceCreationTask.h"
#include <optional>
#include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "net/NetJob.h"
class FlameCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
FlameCreationTask(const QString& staging_path, SettingsObjectPtr global_settings, QWidget* parent)
: InstanceCreationTask(), m_parent(parent)
{
setStagingPath(staging_path);
setParentSettings(global_settings);
}
bool abort() override;
bool updateInstance() override;
bool createInstance() override;
private slots:
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
private:
QWidget* m_parent = nullptr;
shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver;
Flame::Manifest m_pack;
// Handle to allow aborting
NetJob* m_process_update_file_info_job = nullptr;
NetJob::Ptr m_files_job = nullptr;
std::optional<InstancePtr> m_instance;
};

View File

@ -29,21 +29,29 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
} }
} }
static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest) static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
{ {
auto mc = Json::requireObject(manifest, "minecraft"); auto mc = Json::requireObject(manifest, "minecraft");
loadMinecraftV1(m.minecraft, mc);
m.name = Json::ensureString(manifest, QString("name"), "Unnamed"); loadMinecraftV1(pack.minecraft, mc);
m.version = Json::ensureString(manifest, QString("version"), QString());
m.author = Json::ensureString(manifest, QString("author"), "Anonymous"); pack.name = Json::ensureString(manifest, QString("name"), "Unnamed");
pack.version = Json::ensureString(manifest, QString("version"), QString());
pack.author = Json::ensureString(manifest, QString("author"), "Anonymous");
auto arr = Json::ensureArray(manifest, "files", QJsonArray()); auto arr = Json::ensureArray(manifest, "files", QJsonArray());
for (QJsonValueRef item : arr) { for (auto item : arr) {
auto obj = Json::requireObject(item); auto obj = Json::requireObject(item);
Flame::File file; Flame::File file;
loadFileV1(file, obj); loadFileV1(file, obj);
m.files.insert(file.fileId,file);
pack.files.insert(file.fileId,file);
} }
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
pack.overrides = Json::ensureString(manifest, "overrides", "overrides");
pack.is_loaded = true;
} }
void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
@ -61,7 +69,7 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
loadManifestV1(m, obj); loadManifestV1(m, obj);
} }
bool Flame::File::parseFromObject(const QJsonObject& obj) bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked)
{ {
fileName = Json::requireString(obj, "fileName"); fileName = Json::requireString(obj, "fileName");
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
@ -91,7 +99,7 @@ bool Flame::File::parseFromObject(const QJsonObject& obj)
// may throw, if the project is blocked // may throw, if the project is blocked
QString rawUrl = Json::ensureString(obj, "downloadUrl"); QString rawUrl = Json::ensureString(obj, "downloadUrl");
url = QUrl(rawUrl, QUrl::TolerantMode); url = QUrl(rawUrl, QUrl::TolerantMode);
if (!url.isValid()) { if (!url.isValid() && throw_on_blocked) {
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
} }

View File

@ -35,18 +35,18 @@
#pragma once #pragma once
#include <QString>
#include <QVector>
#include <QMap>
#include <QUrl>
#include <QJsonObject> #include <QJsonObject>
#include <QMap>
#include <QString>
#include <QUrl>
#include <QVector>
namespace Flame namespace Flame
{ {
struct File struct File
{ {
// NOTE: throws JSONValidationError // NOTE: throws JSONValidationError
bool parseFromObject(const QJsonObject& object); bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true);
int projectId = 0; int projectId = 0;
int fileId = 0; int fileId = 0;
@ -97,6 +97,8 @@ struct Manifest
//File id -> File //File id -> File
QMap<int,Flame::File> files; QMap<int,Flame::File> files;
QString overrides; QString overrides;
bool is_loaded = false;
}; };
void loadManifest(Flame::Manifest & m, const QString &filepath); void loadManifest(Flame::Manifest & m, const QString &filepath);

View File

@ -0,0 +1,59 @@
#include "OverrideUtils.h"
#include <QDirIterator>
#include "FileSystem.h"
namespace Override {
void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path)
{
QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
if (QFile::exists(file_path))
QFile::remove(file_path);
FS::ensureFilePathExists(file_path);
QFile file(file_path);
file.open(QFile::WriteOnly);
QDirIterator override_iterator(override_path, QDirIterator::Subdirectories);
while (override_iterator.hasNext()) {
auto override_file_path = override_iterator.next();
QFileInfo info(override_file_path);
if (info.isFile()) {
// Absolute path with temp directory -> relative path
override_file_path = override_file_path.split(name).last().remove(0, 1);
file.write(override_file_path.toUtf8());
file.write("\n");
}
}
file.close();
}
QStringList readOverrides(const QString& name, const QString& parent_folder)
{
QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
QFile file(file_path);
if (!file.exists())
return {};
QStringList previous_overrides;
file.open(QFile::ReadOnly);
QString entry;
do {
entry = file.readLine();
previous_overrides.append(entry.trimmed());
} while (!entry.isEmpty());
file.close();
return previous_overrides;
}
} // namespace Override

View File

@ -0,0 +1,20 @@
#pragma once
#include <QString>
namespace Override {
/** This creates a file in `parent_folder` that holds information about which
* overrides are in `override_path`.
*
* If there's already an existing such file, it will be ovewritten.
*/
void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path);
/** This reads an existing overrides archive, returning a list of overrides.
*
* If there's no such file in `parent_folder`, it will return an empty list.
*/
QStringList readOverrides(const QString& name, const QString& parent_folder);
} // namespace Override

View File

@ -59,6 +59,7 @@ void PackFetchTask::fetch()
QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
QObject::connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted);
jobPtr->start(); jobPtr->start();
} }
@ -98,6 +99,14 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch)
delete data; delete data;
}); });
QObject::connect(job, &NetJob::aborted, this, [this, job, data]{
emit aborted();
job->deleteLater();
data->clear();
delete data;
});
job->start(); job->start();
} }
} }
@ -204,4 +213,9 @@ void PackFetchTask::fileDownloadFailed(QString reason)
emit failed(reason); emit failed(reason);
} }
void PackFetchTask::fileDownloadAborted()
{
emit aborted();
}
} }

View File

@ -33,10 +33,12 @@ private:
protected slots: protected slots:
void fileDownloadFinished(); void fileDownloadFinished();
void fileDownloadFailed(QString reason); void fileDownloadFailed(QString reason);
void fileDownloadAborted();
signals: signals:
void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
void failed(QString reason); void failed(QString reason);
void aborted();
void privateFileDownloadFinished(Modpack modpack); void privateFileDownloadFinished(Modpack modpack);
void privateFileDownloadFailed(QString reason, QString packCode); void privateFileDownloadFailed(QString reason, QString packCode);

View File

@ -86,6 +86,7 @@ void PackInstallTask::downloadPack()
connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
netJobContainer->start(); netJobContainer->start();
progress(1, 4); progress(1, 4);
@ -110,6 +111,11 @@ void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10)); setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
} }
void PackInstallTask::onDownloadAborted()
{
emitAborted();
}
void PackInstallTask::unzip() void PackInstallTask::unzip()
{ {
progress(2, 4); progress(2, 4);
@ -228,7 +234,7 @@ void PackInstallTask::install()
progress(4, 4); progress(4, 4);
instance.setName(m_instName); instance.setName(name());
if(m_instIcon == "default") if(m_instIcon == "default")
{ {
m_instIcon = "ftb_logo"; m_instIcon = "ftb_logo";

View File

@ -38,6 +38,7 @@ private slots:
void onDownloadSucceeded(); void onDownloadSucceeded();
void onDownloadFailed(QString reason); void onDownloadFailed(QString reason);
void onDownloadProgress(qint64 current, qint64 total); void onDownloadProgress(qint64 current, qint64 total);
void onDownloadAborted();
void onUnzipFinished(); void onUnzipFinished();
void onUnzipCanceled(); void onUnzipCanceled();

View File

@ -65,9 +65,8 @@ bool PackInstallTask::abort()
if (m_mod_id_resolver_task) if (m_mod_id_resolver_task)
aborted &= m_mod_id_resolver_task->abort(); aborted &= m_mod_id_resolver_task->abort();
// FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet...
if (aborted) if (aborted)
emitFailed(tr("Aborted")); emitAborted();
return aborted; return aborted;
} }
@ -335,7 +334,7 @@ void PackInstallTask::install()
components->saveNow(); components->saveNow();
instance.setName(m_instName); instance.setName(name());
instance.setIconKey(m_instIcon); instance.setIconKey(m_instIcon);
instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name);
instanceSettings->resumeSave(); instanceSettings->resumeSave();

View File

@ -0,0 +1,407 @@
#include "ModrinthInstanceCreationTask.h"
#include "Application.h"
#include "FileSystem.h"
#include "InstanceList.h"
#include "Json.h"
#include "minecraft/PackProfile.h"
#include "modplatform/helpers/OverrideUtils.h"
#include "net/ChecksumValidator.h"
#include "settings/INISettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h"
#include <QAbstractButton>
bool ModrinthCreationTask::abort()
{
if (!canAbort())
return false;
m_abort = true;
if (m_files_job)
m_files_job->abort();
return Task::abort();
}
bool ModrinthCreationTask::updateInstance()
{
auto instance_list = APPLICATION->instances();
// FIXME: How to handle situations when there's more than one install already for a given modpack?
auto inst = instance_list->getInstanceByManagedName(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
if (!inst)
return false;
}
QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (!parseManifest(index_path, m_files, true, false))
return false;
auto version_name = inst->getManagedPackVersionName();
auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : "";
auto info = CustomMessageBox::selectable(
m_parent, tr("Similar modpack was found!"),
tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(version_str),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, tr("Create new instance"));
info->setButtonText(QMessageBox::Abort, tr("Update existing instance"));
info->setButtonText(QMessageBox::Reset, tr("Cancel"));
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok))
return false;
if (info->clickedButton() == info->button(QMessageBox::Reset)) {
m_abort = true;
return false;
}
// Remove repeated files, we don't need to download them!
QDir old_inst_dir(inst->instanceRoot());
QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack"));
QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json"));
QFileInfo old_index_file(old_index_path);
if (old_index_file.exists()) {
std::vector<Modrinth::File> old_files;
parseManifest(old_index_path, old_files, false, false);
// Let's remove all duplicated, identical resources!
auto files_iterator = m_files.begin();
begin:
while (files_iterator != m_files.end()) {
auto const& file = *files_iterator;
auto old_files_iterator = old_files.begin();
while (old_files_iterator != old_files.end()) {
auto const& old_file = *old_files_iterator;
if (old_file.hash == file.hash) {
qDebug() << "Removed file at" << file.path << "from list of downloads";
files_iterator = m_files.erase(files_iterator);
old_files_iterator = old_files.erase(old_files_iterator);
goto begin; // Sorry :c
}
old_files_iterator++;
}
files_iterator++;
}
QDir old_minecraft_dir(inst->gameRoot());
// Some files were removed from the old version, and some will be downloaded in an updated version,
// so we're fine removing them!
if (!old_files.empty()) {
for (auto const& file : old_files) {
if (file.path.isEmpty())
continue;
qDebug() << "Scheduling" << file.path << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path));
}
}
// We will remove all the previous overrides, to prevent duplicate files!
// TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides?
// FIXME: We may want to do something about disabled mods.
auto old_overrides = Override::readOverrides("overrides", old_index_folder);
for (const auto& entry : old_overrides) {
if (entry.isEmpty())
continue;
qDebug() << "Scheduling" << entry << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry));
}
auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder);
for (const auto& entry : old_overrides) {
if (entry.isEmpty())
continue;
qDebug() << "Scheduling" << entry << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry));
}
} else {
// We don't have an old index file, so we may duplicate stuff!
auto dialog = CustomMessageBox::selectable(m_parent,
tr("No index file."),
tr("We couldn't find a suitable index file for the older version. This may cause some of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
if (dialog->exec() == QDialog::DialogCode::Rejected) {
m_abort = true;
return false;
}
}
setOverride(true);
qDebug() << "Will override instance!";
m_instance = inst;
// We let it go through the createInstance() stage, just with a couple modifications for updating
return false;
}
// https://docs.modrinth.com/docs/modpacks/format_definition/
bool ModrinthCreationTask::createInstance()
{
QEventLoop loop;
QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack"));
QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (m_files.empty() && !parseManifest(index_path, m_files, true, true))
return false;
// Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json"));
FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place);
auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft");
auto override_path = FS::PathCombine(m_stagingPath, "overrides");
if (QFile::exists(override_path)) {
// Create a list of overrides in "overrides.txt" inside mrpack/
Override::createOverrides("overrides", parent_folder, override_path);
// Apply the overrides
if (!QFile::rename(override_path, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + "overrides");
return false;
}
}
// Do client overrides
auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides");
if (QFile::exists(client_override_path)) {
// Create a list of overrides in "client-overrides.txt" inside mrpack/
Override::createOverrides("client-overrides", parent_folder, client_override_path);
// Apply the overrides
if (!FS::overrideFolder(mcPath, client_override_path)) {
setError(tr("Could not rename the client overrides folder:\n") + "client overrides");
return false;
}
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", minecraftVersion, true);
if (!fabricVersion.isEmpty())
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (!quiltVersion.isEmpty())
components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion);
if (!forgeVersion.isEmpty())
components->setComponentVersion("net.minecraftforge", forgeVersion);
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
} else {
instance.setIconKey("modrinth");
}
instance.setManagedPack("modrinth", getManagedPackID(), m_managed_name, m_managed_version_id, version());
instance.setName(name());
instance.saveNow();
m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
for (auto file : m_files) {
auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path);
qDebug() << "Will try to download" << file.downloads.front() << "to" << path;
auto dl = Net::Download::makeFile(file.downloads.dequeue(), path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(dl);
if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef();
connect(dl.get(), &NetAction::failed, [this, &file, path, param] {
auto ndl = Net::Download::makeFile(file.downloads.dequeue(), path);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(ndl);
if (auto shared = param.lock()) shared->succeeded();
});
}
}
bool ended_well = false;
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; });
connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) {
ended_well = false;
setError(reason);
});
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); });
setStatus(tr("Downloading mods..."));
m_files_job->start();
loop.exec();
// Update information of the already installed instance, if any.
if (m_instance && ended_well) {
setAbortable(false);
auto inst = m_instance.value();
// Only change the name if it didn't use a custom name, so that the previous custom name
// is preserved, but if we're using the original one, we update the version string.
// NOTE: This needs to come before the copyManagedPack call!
if (inst->name().contains(inst->getManagedPackVersionName())) {
if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange)
inst->setName(instance.name());
}
inst->copyManagedPack(instance);
}
return ended_well;
}
bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<Modrinth::File>& files, bool set_managed_info, bool show_optional_dialog)
{
try {
auto doc = Json::requireDocument(index_path);
auto obj = Json::requireObject(doc, "modrinth.index.json");
int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
if (formatVersion == 1) {
auto game = Json::requireString(obj, "game", "modrinth.index.json");
if (game != "minecraft") {
throw JSONValidationError("Unknown game: " + game);
}
if (set_managed_info) {
m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID");
m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name");
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
bool had_optional = false;
for (const auto& modInfo : jsonFiles) {
Modrinth::File file;
file.path = Json::requireString(modInfo, "path");
auto env = Json::ensureObject(modInfo, "env");
// 'env' field is optional
if (!env.isEmpty()) {
QString support = Json::ensureString(env, "client", "unsupported");
if (support == "unsupported") {
continue;
} else if (support == "optional") {
// TODO: Make a review dialog for choosing which ones the user wants!
if (!had_optional && show_optional_dialog) {
had_optional = true;
auto info = CustomMessageBox::selectable(
m_parent, tr("Optional mod detected!"),
tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"),
QMessageBox::Information);
info->exec();
}
if (file.path.endsWith(".jar"))
file.path += ".disabled";
}
}
QJsonObject hashes = Json::requireObject(modInfo, "hashes");
QString hash;
QCryptographicHash::Algorithm hashAlgorithm;
hash = Json::ensureString(hashes, "sha1");
hashAlgorithm = QCryptographicHash::Sha1;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha512");
hashAlgorithm = QCryptographicHash::Sha512;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha256");
hashAlgorithm = QCryptographicHash::Sha256;
if (hash.isEmpty()) {
throw JSONValidationError("No hash found for: " + file.path);
}
}
}
file.hash = QByteArray::fromHex(hash.toLatin1());
file.hashAlgorithm = hashAlgorithm;
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)
auto download_arr = Json::ensureArray(modInfo, "downloads");
for (auto download : download_arr) {
qWarning() << download.toString();
bool is_last = download.toString() == download_arr.last().toString();
auto download_url = QUrl(download.toString());
if (!download_url.isValid()) {
qDebug()
<< QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(download_url.toString(), file.path);
if (is_last && file.downloads.isEmpty())
throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
} else {
file.downloads.push_back(download_url);
}
}
files.push_back(file);
}
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key();
if (name == "minecraft") {
minecraftVersion = Json::requireString(*it, "Minecraft version");
} else if (name == "fabric-loader") {
fabricVersion = Json::requireString(*it, "Fabric Loader version");
} else if (name == "quilt-loader") {
quiltVersion = Json::requireString(*it, "Quilt Loader version");
} else if (name == "forge") {
forgeVersion = Json::requireString(*it, "Forge version");
} else {
throw JSONValidationError("Unknown dependency type: " + name);
}
}
} else {
throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion));
}
} catch (const JSONValidationError& e) {
setError(tr("Could not understand pack index:\n") + e.cause());
return false;
}
return true;
}
QString ModrinthCreationTask::getManagedPackID() const
{
if (!m_source_url.isEmpty()) {
QRegularExpression regex(R"(data\/(.*)\/versions)");
return regex.match(m_source_url).captured(1);
}
return {};
}

View File

@ -0,0 +1,44 @@
#pragma once
#include "InstanceCreationTask.h"
#include <optional>
#include "minecraft/MinecraftInstance.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/NetJob.h"
class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
ModrinthCreationTask(QString staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString source_url = {})
: InstanceCreationTask(), m_parent(parent), m_source_url(std::move(source_url))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
}
bool abort() override;
bool updateInstance() override;
bool createInstance() override;
private:
bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_managed_info = true, bool show_optional_dialog = true);
QString getManagedPackID() const;
private:
QWidget* m_parent = nullptr;
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
QString m_managed_id, m_managed_version_id, m_managed_name;
QString m_source_url;
std::vector<Modrinth::File> m_files;
NetJob::Ptr m_files_job;
std::optional<InstancePtr> m_instance;
};

View File

@ -133,7 +133,7 @@ void Technic::SingleZipPackInstallTask::extractFinished()
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion);
} }
void Technic::SingleZipPackInstallTask::extractAborted() void Technic::SingleZipPackInstallTask::extractAborted()

View File

@ -77,6 +77,7 @@ void Technic::SolderPackInstallTask::executeTask()
auto job = m_filesNetJob.get(); auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
m_filesNetJob->start(); m_filesNetJob->start();
} }
@ -127,6 +128,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
m_filesNetJob->start(); m_filesNetJob->start();
} }
@ -171,6 +173,12 @@ void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qin
setProgress(current / 2, total); setProgress(current / 2, total);
} }
void Technic::SolderPackInstallTask::downloadAborted()
{
emitAborted();
m_filesNetJob.reset();
}
void Technic::SolderPackInstallTask::extractFinished() void Technic::SolderPackInstallTask::extractFinished()
{ {
if (!m_extractFuture.result()) if (!m_extractFuture.result())
@ -214,7 +222,7 @@ void Technic::SolderPackInstallTask::extractFinished()
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true);
} }
void Technic::SolderPackInstallTask::extractAborted() void Technic::SolderPackInstallTask::extractAborted()

View File

@ -61,6 +61,7 @@ namespace Technic
void downloadSucceeded(); void downloadSucceeded();
void downloadFailed(QString reason); void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total); void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished(); void extractFinished();
void extractAborted(); void extractAborted();

View File

@ -68,7 +68,7 @@ class Task : public QObject, public QRunnable {
virtual QStringList warnings() const; virtual QStringList warnings() const;
virtual bool canAbort() const { return false; } virtual bool canAbort() const { return m_can_abort; }
auto getState() const -> State { return m_state; } auto getState() const -> State { return m_state; }
@ -96,6 +96,10 @@ class Task : public QObject, public QRunnable {
void status(QString status); void status(QString status);
void stepStatus(QString status); void stepStatus(QString status);
/** Emitted when the canAbort() status has changed.
*/
void abortStatusChanged(bool can_abort);
public slots: public slots:
// QRunnable's interface // QRunnable's interface
void run() override { start(); } void run() override { start(); }
@ -103,6 +107,8 @@ class Task : public QObject, public QRunnable {
virtual void start(); virtual void start();
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
void setAbortable(bool can_abort) { m_can_abort = can_abort; emit abortStatusChanged(can_abort); }
protected: protected:
virtual void executeTask() = 0; virtual void executeTask() = 0;
@ -125,4 +131,8 @@ class Task : public QObject, public QRunnable {
// TODO: Nuke in favor of QLoggingCategory // TODO: Nuke in favor of QLoggingCategory
bool m_show_debug = true; bool m_show_debug = true;
private:
// Change using setAbortStatus
bool m_can_abort = false;
}; };

View File

@ -86,6 +86,10 @@ struct Language
else { else {
result = locale.nativeLanguageName(); result = locale.nativeLanguageName();
} }
if (result.isEmpty()) {
result = key;
}
return result; return result;
} }
@ -394,7 +398,7 @@ void TranslationsModel::reloadLocalFiles()
return false; return false;
} }
} }
return a.key < b.key; return a.languageName().toLower() < b.languageName().toLower();
}); });
endInsertRows(); endInsertRows();
} }

View File

@ -95,8 +95,14 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent)
m_launchOfflineButton = new QPushButton(); m_launchOfflineButton = new QPushButton();
horizontalLayout->addWidget(m_launchOfflineButton); horizontalLayout->addWidget(m_launchOfflineButton);
m_launchOfflineButton->setText(tr("Launch Offline")); m_launchOfflineButton->setText(tr("Launch Offline"));
m_launchDemoButton = new QPushButton();
horizontalLayout->addWidget(m_launchDemoButton);
m_launchDemoButton->setText(tr("Launch Demo"));
updateLaunchButtons(); updateLaunchButtons();
connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked())); connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked()));
connect(m_launchDemoButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftDemo_clicked()));
m_closeButton = new QPushButton(); m_closeButton = new QPushButton();
m_closeButton->setText(tr("Close")); m_closeButton->setText(tr("Close"));
@ -143,6 +149,7 @@ void InstanceWindow::updateLaunchButtons()
if(m_instance->isRunning()) if(m_instance->isRunning())
{ {
m_launchOfflineButton->setEnabled(false); m_launchOfflineButton->setEnabled(false);
m_launchDemoButton->setEnabled(false);
m_killButton->setText(tr("Kill")); m_killButton->setText(tr("Kill"));
m_killButton->setObjectName("killButton"); m_killButton->setObjectName("killButton");
m_killButton->setToolTip(tr("Kill the running instance")); m_killButton->setToolTip(tr("Kill the running instance"));
@ -150,6 +157,7 @@ void InstanceWindow::updateLaunchButtons()
else if(!m_instance->canLaunch()) else if(!m_instance->canLaunch())
{ {
m_launchOfflineButton->setEnabled(false); m_launchOfflineButton->setEnabled(false);
m_launchDemoButton->setEnabled(false);
m_killButton->setText(tr("Launch")); m_killButton->setText(tr("Launch"));
m_killButton->setObjectName("launchButton"); m_killButton->setObjectName("launchButton");
m_killButton->setToolTip(tr("Launch the instance")); m_killButton->setToolTip(tr("Launch the instance"));
@ -158,6 +166,13 @@ void InstanceWindow::updateLaunchButtons()
else else
{ {
m_launchOfflineButton->setEnabled(true); m_launchOfflineButton->setEnabled(true);
// Disable demo-mode if not available.
auto instance = dynamic_cast<MinecraftInstance*>(m_instance.get());
if (instance) {
m_launchDemoButton->setEnabled(instance->supportsDemo());
}
m_killButton->setText(tr("Launch")); m_killButton->setText(tr("Launch"));
m_killButton->setObjectName("launchButton"); m_killButton->setObjectName("launchButton");
m_killButton->setToolTip(tr("Launch the instance")); m_killButton->setToolTip(tr("Launch the instance"));
@ -169,7 +184,12 @@ void InstanceWindow::updateLaunchButtons()
void InstanceWindow::on_btnLaunchMinecraftOffline_clicked() void InstanceWindow::on_btnLaunchMinecraftOffline_clicked()
{ {
APPLICATION->launch(m_instance, false, nullptr); APPLICATION->launch(m_instance, false, false, nullptr);
}
void InstanceWindow::on_btnLaunchMinecraftDemo_clicked()
{
APPLICATION->launch(m_instance, false, true, nullptr);
} }
void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc) void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc)
@ -223,7 +243,7 @@ void InstanceWindow::on_btnKillMinecraft_clicked()
} }
else else
{ {
APPLICATION->launch(m_instance, true, nullptr); APPLICATION->launch(m_instance, true, false, nullptr);
} }
} }

View File

@ -74,6 +74,7 @@ slots:
void on_closeButton_clicked(); void on_closeButton_clicked();
void on_btnKillMinecraft_clicked(); void on_btnKillMinecraft_clicked();
void on_btnLaunchMinecraftOffline_clicked(); void on_btnLaunchMinecraftOffline_clicked();
void on_btnLaunchMinecraftDemo_clicked();
void instanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc); void instanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc);
void runningStateChanged(bool running); void runningStateChanged(bool running);
@ -93,4 +94,5 @@ private:
QPushButton *m_closeButton = nullptr; QPushButton *m_closeButton = nullptr;
QPushButton *m_killButton = nullptr; QPushButton *m_killButton = nullptr;
QPushButton *m_launchOfflineButton = nullptr; QPushButton *m_launchOfflineButton = nullptr;
QPushButton *m_launchDemoButton = nullptr;
}; };

View File

@ -240,6 +240,7 @@ public:
TranslatedAction actionCAT; TranslatedAction actionCAT;
TranslatedAction actionCopyInstance; TranslatedAction actionCopyInstance;
TranslatedAction actionLaunchInstanceOffline; TranslatedAction actionLaunchInstanceOffline;
TranslatedAction actionLaunchInstanceDemo;
TranslatedAction actionScreenshots; TranslatedAction actionScreenshots;
TranslatedAction actionExportInstance; TranslatedAction actionExportInstance;
QVector<TranslatedAction *> all_actions; QVector<TranslatedAction *> all_actions;
@ -499,6 +500,7 @@ public:
fileMenu->addAction(actionAddInstance); fileMenu->addAction(actionAddInstance);
fileMenu->addAction(actionLaunchInstance); fileMenu->addAction(actionLaunchInstance);
fileMenu->addAction(actionLaunchInstanceOffline); fileMenu->addAction(actionLaunchInstanceOffline);
fileMenu->addAction(actionLaunchInstanceDemo);
fileMenu->addAction(actionKillInstance); fileMenu->addAction(actionKillInstance);
fileMenu->addAction(actionCloseWindow); fileMenu->addAction(actionCloseWindow);
fileMenu->addSeparator(); fileMenu->addSeparator();
@ -669,6 +671,12 @@ public:
actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode.")); actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode."));
all_actions.append(&actionLaunchInstanceOffline); all_actions.append(&actionLaunchInstanceOffline);
actionLaunchInstanceDemo = TranslatedAction(MainWindow);
actionLaunchInstanceDemo->setObjectName(QStringLiteral("actionLaunchInstanceDemo"));
actionLaunchInstanceDemo.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Demo"));
actionLaunchInstanceDemo.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in demo mode."));
all_actions.append(&actionLaunchInstanceDemo);
actionKillInstance = TranslatedAction(MainWindow); actionKillInstance = TranslatedAction(MainWindow);
actionKillInstance->setObjectName(QStringLiteral("actionKillInstance")); actionKillInstance->setObjectName(QStringLiteral("actionKillInstance"));
actionKillInstance->setDisabled(true); actionKillInstance->setDisabled(true);
@ -1195,6 +1203,7 @@ void MainWindow::updateToolsMenu()
ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning); ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning);
ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning); ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning);
ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning);
QMenu *launchMenu = ui->actionLaunchInstance->menu(); QMenu *launchMenu = ui->actionLaunchInstance->menu();
QMenu *launchOfflineMenu = ui->actionLaunchInstanceOffline->menu(); QMenu *launchOfflineMenu = ui->actionLaunchInstanceOffline->menu();
@ -1220,23 +1229,37 @@ void MainWindow::updateToolsMenu()
normalLaunch->setShortcut(QKeySequence::Open); normalLaunch->setShortcut(QKeySequence::Open);
QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline")); QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline"));
normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O")));
QAction *normalLaunchDemo = launchOfflineMenu->addAction(tr("Launch Demo"));
normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O")));
if (m_selectedInstance) if (m_selectedInstance)
{ {
normalLaunch->setEnabled(m_selectedInstance->canLaunch()); normalLaunch->setEnabled(m_selectedInstance->canLaunch());
normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch()); normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch());
normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch());
connect(normalLaunch, &QAction::triggered, [this]() { connect(normalLaunch, &QAction::triggered, [this]() {
APPLICATION->launch(m_selectedInstance, true); APPLICATION->launch(m_selectedInstance, true, false);
}); });
connect(normalLaunchOffline, &QAction::triggered, [this]() { connect(normalLaunchOffline, &QAction::triggered, [this]() {
APPLICATION->launch(m_selectedInstance, false); APPLICATION->launch(m_selectedInstance, false, false);
});
connect(normalLaunchDemo, &QAction::triggered, [this]() {
APPLICATION->launch(m_selectedInstance, false, true);
}); });
} }
else else
{ {
normalLaunch->setDisabled(true); normalLaunch->setDisabled(true);
normalLaunchOffline->setDisabled(true); normalLaunchOffline->setDisabled(true);
normalLaunchDemo->setDisabled(true);
} }
// Disable demo-mode if not available.
auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get());
if (instance) {
normalLaunchDemo->setEnabled(instance->supportsDemo());
}
QString profilersTitle = tr("Profilers"); QString profilersTitle = tr("Profilers");
launchMenu->addSeparator()->setText(profilersTitle); launchMenu->addSeparator()->setText(profilersTitle);
launchOfflineMenu->addSeparator()->setText(profilersTitle); launchOfflineMenu->addSeparator()->setText(profilersTitle);
@ -1260,11 +1283,11 @@ void MainWindow::updateToolsMenu()
connect(profilerAction, &QAction::triggered, [this, profiler]() connect(profilerAction, &QAction::triggered, [this, profiler]()
{ {
APPLICATION->launch(m_selectedInstance, true, profiler.get()); APPLICATION->launch(m_selectedInstance, true, false, profiler.get());
}); });
connect(profilerOfflineAction, &QAction::triggered, [this, profiler]() connect(profilerOfflineAction, &QAction::triggered, [this, profiler]()
{ {
APPLICATION->launch(m_selectedInstance, false, profiler.get()); APPLICATION->launch(m_selectedInstance, false, false, profiler.get());
}); });
} }
else else
@ -1633,6 +1656,10 @@ void MainWindow::runModalTask(Task *task)
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
} }
}); });
connect(task, &Task::aborted, [this]
{
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show();
});
ProgressDialog loadDialog(this); ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort")); loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task); loadDialog.execWithTask(task);
@ -2096,6 +2123,14 @@ void MainWindow::on_actionLaunchInstanceOffline_triggered()
} }
} }
void MainWindow::on_actionLaunchInstanceDemo_triggered()
{
if (m_selectedInstance)
{
APPLICATION->launch(m_selectedInstance, false, true);
}
}
void MainWindow::on_actionKillInstance_triggered() void MainWindow::on_actionKillInstance_triggered()
{ {
if(m_selectedInstance && m_selectedInstance->isRunning()) if(m_selectedInstance && m_selectedInstance->isRunning())
@ -2139,6 +2174,14 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
ui->setInstanceActionsEnabled(true); ui->setInstanceActionsEnabled(true);
ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch());
ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch());
ui->actionLaunchInstanceDemo->setEnabled(m_selectedInstance->canLaunch());
// Disable demo-mode if not available.
auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get());
if (instance) {
ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo());
}
ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning());
ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport());
ui->renameButton->setText(m_selectedInstance->name()); ui->renameButton->setText(m_selectedInstance->name());
@ -2158,6 +2201,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
ui->setInstanceActionsEnabled(false); ui->setInstanceActionsEnabled(false);
ui->actionLaunchInstance->setEnabled(false); ui->actionLaunchInstance->setEnabled(false);
ui->actionLaunchInstanceOffline->setEnabled(false); ui->actionLaunchInstanceOffline->setEnabled(false);
ui->actionLaunchInstanceDemo->setEnabled(false);
ui->actionKillInstance->setEnabled(false); ui->actionKillInstance->setEnabled(false);
APPLICATION->settings()->set("SelectedInstance", QString()); APPLICATION->settings()->set("SelectedInstance", QString());
selectionBad(); selectionBad();

View File

@ -140,6 +140,8 @@ private slots:
void on_actionLaunchInstanceOffline_triggered(); void on_actionLaunchInstanceOffline_triggered();
void on_actionLaunchInstanceDemo_triggered();
void on_actionKillInstance_triggered(); void on_actionKillInstance_triggered();
void on_actionDeleteInstance_triggered(); void on_actionDeleteInstance_triggered();

View File

@ -51,6 +51,7 @@
#include <QFileDialog> #include <QFileDialog>
#include <QValidator> #include <QValidator>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <utility>
#include "ui/widgets/PageContainer.h" #include "ui/widgets/PageContainer.h"
#include "ui/pages/modplatform/VanillaPage.h" #include "ui/pages/modplatform/VanillaPage.h"
@ -180,10 +181,27 @@ NewInstanceDialog::~NewInstanceDialog()
void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task) void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task)
{ {
creationTask.reset(task); creationTask.reset(task);
ui->instNameTextBox->setPlaceholderText(name);
if(!task) ui->instNameTextBox->setPlaceholderText(name);
{ importVersion.clear();
if (!task) {
ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
importIcon = false;
}
auto allowOK = task && !instName().isEmpty();
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
}
void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, InstanceTask* task)
{
creationTask.reset(task);
ui->instNameTextBox->setPlaceholderText(name);
importVersion = std::move(version);
if (!task) {
ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
importIcon = false; importIcon = false;
} }
@ -214,7 +232,11 @@ InstanceTask * NewInstanceDialog::extractTask()
{ {
InstanceTask * extracted = creationTask.get(); InstanceTask * extracted = creationTask.get();
creationTask.release(); creationTask.release();
extracted->setName(instName());
InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion);
inst_name.setName(ui->instNameTextBox->text().trimmed());
extracted->setName(inst_name);
extracted->setGroup(instGroup()); extracted->setGroup(instGroup());
extracted->setIcon(iconKey()); extracted->setIcon(iconKey());
return extracted; return extracted;

View File

@ -37,7 +37,6 @@
#include <QDialog> #include <QDialog>
#include "BaseVersion.h"
#include "ui/pages/BasePageProvider.h" #include "ui/pages/BasePageProvider.h"
#include "InstanceTask.h" #include "InstanceTask.h"
@ -61,7 +60,8 @@ public:
void updateDialogState(); void updateDialogState();
void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr); void setSuggestedPack(const QString& name = QString(), InstanceTask * task = nullptr);
void setSuggestedPack(const QString& name, QString version, InstanceTask * task = nullptr);
void setSuggestedIconFromFile(const QString &path, const QString &name); void setSuggestedIconFromFile(const QString &path, const QString &name);
void setSuggestedIcon(const QString &key); void setSuggestedIcon(const QString &key);
@ -95,5 +95,7 @@ private:
QString importIconPath; QString importIconPath;
QString importIconName; QString importIconName;
QString importVersion;
void importIconNow(); void importIconNow();
}; };

View File

@ -43,8 +43,7 @@ void ProgressDialog::setSkipButton(bool present, QString label)
void ProgressDialog::on_skipButton_clicked(bool checked) void ProgressDialog::on_skipButton_clicked(bool checked)
{ {
Q_UNUSED(checked); Q_UNUSED(checked);
if (task->abort()) task->abort();
QDialog::reject();
} }
ProgressDialog::~ProgressDialog() ProgressDialog::~ProgressDialog()
@ -81,7 +80,8 @@ int ProgressDialog::execWithTask(Task* task)
connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus); connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus);
connect(task, &Task::progress, this, &ProgressDialog::changeProgress); connect(task, &Task::progress, this, &ProgressDialog::changeProgress);
connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); connect(task, &Task::aborted, [this] { QDialog::reject(); });
connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled);
m_is_multi_step = task->isMultiStep(); m_is_multi_step = task->isMultiStep();
if (!m_is_multi_step) { if (!m_is_multi_step) {

View File

@ -811,7 +811,7 @@ void ServersPage::on_actionMove_Down_triggered()
void ServersPage::on_actionJoin_triggered() void ServersPage::on_actionJoin_triggered()
{ {
const auto &address = m_model->at(currentServer)->m_address; const auto &address = m_model->at(currentServer)->m_address;
APPLICATION->launch(m_inst, true, nullptr, std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(address))); APPLICATION->launch(m_inst, true, false, nullptr, std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(address)));
} }
#include "ServersPage.moc" #include "ServersPage.moc"

View File

@ -39,6 +39,7 @@
#include "ui_ExternalResourcesPage.h" #include "ui_ExternalResourcesPage.h"
#include "minecraft/mod/TexturePackFolderModel.h" #include "minecraft/mod/TexturePackFolderModel.h"
#include "minecraft/mod/TexturePack.h"
class TexturePackPage : public ExternalResourcesPage class TexturePackPage : public ExternalResourcesPage
{ {
@ -60,4 +61,15 @@ public:
{ {
return m_instance->traits().contains("texturepacks"); return m_instance->traits().contains("texturepacks");
} }
public slots:
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
auto& rp = static_cast<TexturePack&>(m_model->at(row));
ui->frame->updateWithTexturePack(rp);
return true;
}
}; };

View File

@ -60,6 +60,7 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true); m_search_timer.setSingleShot(true);

View File

@ -39,12 +39,12 @@
#include <QTabBar> #include <QTabBar>
#include "Application.h" #include "Application.h"
#include "Filter.h"
#include "Version.h"
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/VersionList.h" #include "meta/VersionList.h"
#include "minecraft/VanillaInstanceCreationTask.h"
#include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewInstanceDialog.h"
#include "Filter.h"
#include "InstanceCreationTask.h"
#include "Version.h"
VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
: QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage)
@ -217,11 +217,11 @@ void VanillaPage::suggestCurrent()
// There isn't a selected version if the version list is empty // There isn't a selected version if the version list is empty
if(ui->loaderVersionList->selectedVersion() == nullptr) if(ui->loaderVersionList->selectedVersion() == nullptr)
dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); dialog->setSuggestedPack(m_selectedVersion->descriptor(), new VanillaCreationTask(m_selectedVersion));
else else
{ {
dialog->setSuggestedPack(m_selectedVersion->descriptor(), dialog->setSuggestedPack(m_selectedVersion->descriptor(),
new InstanceCreationTask(m_selectedVersion, m_selectedLoader, new VanillaCreationTask(m_selectedVersion, m_selectedLoader,
m_selectedLoaderVersion)); m_selectedLoaderVersion));
} }
dialog->setSuggestedIcon("default"); dialog->setSuggestedIcon("default");

View File

@ -117,7 +117,7 @@ void AtlPage::suggestCurrent()
} }
auto uiSupport = new AtlUserInteractionSupportImpl(this); auto uiSupport = new AtlUserInteractionSupportImpl(this);
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion)); dialog->setSuggestedPack(selected.name, selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion));
auto editedLogoName = selected.safeName; auto editedLogoName = selected.safeName;
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());

View File

@ -127,7 +127,7 @@ void FtbPage::suggestCurrent()
return; return;
} }
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this)); dialog->setSuggestedPack(selected.name, selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this));
for(auto art : selected.art) { for(auto art : selected.art) {
if(art.type == "square") { if(art.type == "square") {
QString editedLogoName; QString editedLogoName;

View File

@ -146,6 +146,7 @@ void Page::openedImpl()
{ {
connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully); connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully);
connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed); connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed);
connect(ftbFetchTask.get(), &PackFetchTask::aborted, this, &Page::ftbPackDataDownloadAborted);
connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully); connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully);
connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed); connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed);
@ -176,7 +177,7 @@ void Page::suggestCurrent()
return; return;
} }
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); dialog->setSuggestedPack(selected.name, selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
QString editedLogoName; QString editedLogoName;
if(selected.logo.toLower().startsWith("ftb")) if(selected.logo.toLower().startsWith("ftb"))
{ {
@ -220,7 +221,12 @@ void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList
void Page::ftbPackDataDownloadFailed(QString reason) void Page::ftbPackDataDownloadFailed(QString reason)
{ {
//TODO: Display the error CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
}
void Page::ftbPackDataDownloadAborted()
{
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show();
} }
void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack)

View File

@ -95,6 +95,7 @@ private:
private slots: private slots:
void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks); void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks);
void ftbPackDataDownloadFailed(QString reason); void ftbPackDataDownloadFailed(QString reason);
void ftbPackDataDownloadAborted();
void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); void ftbPrivatePackDataDownloadSuccessfully(Modpack pack);
void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);

View File

@ -294,7 +294,7 @@ void ModrinthPage::suggestCurrent()
for (auto& ver : current.versions) { for (auto& ver : current.versions) {
if (ver.id == selectedVersion) { if (ver.id == selectedVersion) {
dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url, this)); dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this));
auto iconName = current.iconName; auto iconName = current.iconName;
m_model->getLogo(iconName, current.iconUrl.toString(), m_model->getLogo(iconName, current.iconUrl.toString(),
[this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); });

View File

@ -271,11 +271,11 @@ void TechnicPage::selectVersion() {
if (!current.isSolder) if (!current.isSolder)
{ {
dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); dialog->setSuggestedPack(current.name, selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
} }
else else
{ {
dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion)); dialog->setSuggestedPack(current.name, selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion));
} }
} }

View File

@ -105,10 +105,7 @@ static const QMap<QChar, QString> s_value_to_color = {
{'f', "#FFFFFF"} {'f', "#FFFFFF"}
}; };
void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) QString InfoFrame::renderColorCodes(QString input) {
{
setName(resource_pack.name());
// We have to manually set the colors for use. // We have to manually set the colors for use.
// //
// A color is set using §x, with x = a hex number from 0 to f. // A color is set using §x, with x = a hex number from 0 to f.
@ -119,39 +116,49 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
// TODO: Make the same logic for font formatting too. // TODO: Make the same logic for font formatting too.
// TODO: Wrap links inside <a> tags // TODO: Wrap links inside <a> tags
auto description = resource_pack.description(); QString html("<html>");
QString description_parsed("<html>");
bool in_div = false; bool in_div = false;
auto desc_it = description.constBegin(); auto it = input.constBegin();
while (desc_it != description.constEnd()) { while (it != input.constEnd()) {
if (*desc_it == u'§') { if (*it == u'§') {
if (in_div) if (in_div)
description_parsed += "</span>"; html += "</span>";
auto const& num = *(++desc_it); auto const& num = *(++it);
description_parsed += QString("<span style=\"color: %1;\">").arg(s_value_to_color.constFind(num).value()); html += QString("<span style=\"color: %1;\">").arg(s_value_to_color.constFind(num).value());
in_div = true; in_div = true;
desc_it++; it++;
} }
description_parsed += *desc_it; html += *it;
desc_it++; it++;
} }
if (in_div) if (in_div)
description_parsed += "</span>"; html += "</span>";
description_parsed += "</html>"; html += "</html>";
description_parsed.replace("\n", "<br>"); html.replace("\n", "<br>");
return html;
}
setDescription(description_parsed); void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
{
setName(resource_pack.name());
setDescription(renderColorCodes(resource_pack.description()));
setImage(resource_pack.image({64, 64})); setImage(resource_pack.image({64, 64}));
} }
void InfoFrame::updateWithTexturePack(TexturePack& texture_pack)
{
setName(texture_pack.name());
setDescription(renderColorCodes(texture_pack.description()));
setImage(texture_pack.image({64, 64}));
}
void InfoFrame::clear() void InfoFrame::clear()
{ {
setName(); setName();

View File

@ -19,6 +19,7 @@
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/TexturePack.h"
namespace Ui namespace Ui
{ {
@ -41,6 +42,9 @@ class InfoFrame : public QFrame {
void updateWithMod(Mod const& m); void updateWithMod(Mod const& m);
void updateWithResource(Resource const& resource); void updateWithResource(Resource const& resource);
void updateWithResourcePack(ResourcePack& rp); void updateWithResourcePack(ResourcePack& rp);
void updateWithTexturePack(TexturePack& tp);
static QString renderColorCodes(QString input);
public slots: public slots:
void descriptionEllipsisHandler(QString link); void descriptionEllipsisHandler(QString link);

View File

@ -63,7 +63,8 @@ public final class LegacyFrame extends Frame {
int winSizeH, int winSizeH,
boolean maximize, boolean maximize,
String serverAddress, String serverAddress,
String serverPort String serverPort,
boolean isDemo
) { ) {
// Implements support for launching in to multiplayer on classic servers using a mpticket // Implements support for launching in to multiplayer on classic servers using a mpticket
// file generated by an external program and stored in the instance's root folder. // file generated by an external program and stored in the instance's root folder.
@ -106,7 +107,7 @@ public final class LegacyFrame extends Frame {
appletWrap.setParameter("sessionid", session); appletWrap.setParameter("sessionid", session);
appletWrap.setParameter("stand-alone", "true"); // Show the quit button. appletWrap.setParameter("stand-alone", "true"); // Show the quit button.
appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work. appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work.
appletWrap.setParameter("demo", "false"); appletWrap.setParameter("demo", isDemo ? "true" : "false");
appletWrap.setParameter("fullscreen", "false"); appletWrap.setParameter("fullscreen", "false");
add(appletWrap); add(appletWrap);

View File

@ -24,8 +24,8 @@ import java.applet.Applet;
import java.io.File; import java.io.File;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -58,10 +58,10 @@ public final class OneSixLauncher implements Launcher {
public OneSixLauncher(Parameters params) { public OneSixLauncher(Parameters params) {
classLoader = ClassLoader.getSystemClassLoader(); classLoader = ClassLoader.getSystemClassLoader();
mcParams = params.allSafe("param", Collections.<String>emptyList()); mcParams = params.allSafe("param", new ArrayList<String>());
mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft");
appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet");
traits = params.allSafe("traits", Collections.<String>emptyList()); traits = params.allSafe("traits", new ArrayList<String>());
userName = params.first("userName"); userName = params.first("userName");
sessionId = params.first("sessionId"); sessionId = params.first("sessionId");
@ -137,7 +137,8 @@ public final class OneSixLauncher implements Launcher {
winSizeH, winSizeH,
maximize, maximize,
serverAddress, serverAddress,
serverPort serverPort,
mcParams.contains("--demo")
); );
return; return;

View File

@ -24,6 +24,9 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V
ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ResourcePackParse) TEST_NAME ResourcePackParse)
ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME TexturePackParse)
ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ParseUtils) TEST_NAME ParseUtils)

View File

@ -4,6 +4,8 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <pathmatcher/RegexpMatcher.h>
class FileSystemTest : public QObject class FileSystemTest : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -111,6 +113,40 @@ slots:
f(); f();
} }
void test_copy_with_blacklist()
{
QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder");
auto f = [&folder]()
{
QTemporaryDir tempDir;
tempDir.setAutoRemove(true);
qDebug() << "From:" << folder << "To:" << tempDir.path();
QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder"));
qDebug() << tempDir.path();
qDebug() << target_dir.path();
FS::copy c(folder, target_dir.path());
c.blacklist(new RegexpMatcher("[.]?mcmeta"));
c();
for(auto entry: target_dir.entryList())
{
qDebug() << entry;
}
QVERIFY(!target_dir.entryList().contains("pack.mcmeta"));
QVERIFY(target_dir.entryList().contains("assets"));
};
// first try variant without trailing /
QVERIFY(!folder.endsWith('/'));
f();
// then variant with trailing /
folder.append('/');
QVERIFY(folder.endsWith('/'));
f();
}
void test_getDesktop() void test_getDesktop()
{ {
QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));

View File

@ -146,7 +146,6 @@ slots:
for (auto mod : model.allMods()) for (auto mod : model.allMods())
qDebug() << mod->name(); qDebug() << mod->name();
// FIXME: It considers every file in the directory as a mod, but we should probably filter that out somehow.
QCOMPARE(model.size(), 4); QCOMPARE(model.size(), 4);
model.stopWatching(); model.stopWatching();

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <QTest>
#include <QTimer>
#include "FileSystem.h"
#include "minecraft/mod/TexturePack.h"
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
class TexturePackParseTest : public QObject {
Q_OBJECT
private slots:
void test_parseZIP()
{
QString source = QFINDTESTDATA("testdata/TexturePackParse");
QString zip_rp = FS::PathCombine(source, "test_texture_pack_idk.zip");
TexturePack pack { QFileInfo(zip_rp) };
TexturePackUtils::processZIP(pack);
QVERIFY(pack.description() == "joe biden, wake up");
}
void test_parseFolder()
{
QString source = QFINDTESTDATA("testdata/TexturePackParse");
QString folder_rp = FS::PathCombine(source, "test_texturefolder");
TexturePack pack { QFileInfo(folder_rp) };
TexturePackUtils::processFolder(pack);
QVERIFY(pack.description() == "Some texture pack surely");
}
void test_parseFolder2()
{
QString source = QFINDTESTDATA("testdata/TexturePackParse");
QString folder_rp = FS::PathCombine(source, "another_test_texturefolder");
TexturePack pack { QFileInfo(folder_rp) };
TexturePackUtils::process(pack);
QVERIFY(pack.description() == "quieres\nfor real");
}
};
QTEST_GUILESS_MAIN(TexturePackParseTest)
#include "TexturePackParse_test.moc"

Binary file not shown.

Binary file not shown.

Binary file not shown.