Merge remote-tracking branch 'origin/develop' into download-all-blocked

Signed-off-by: kumquat-ir <66188216+kumquat-ir@users.noreply.github.com>
This commit is contained in:
kumquat-ir
2022-07-31 14:54:50 -04:00
60 changed files with 1019 additions and 1299 deletions

View File

@ -117,6 +117,7 @@ set(NET_SOURCES
net/NetAction.h
net/NetJob.cpp
net/NetJob.h
net/NetUtils.h
net/PasteUpload.cpp
net/PasteUpload.h
net/Sink.h
@ -990,7 +991,6 @@ target_link_libraries(Launcher_logic
Launcher_murmur2
nbt++
${ZLIB_LIBRARIES}
optional-bare
tomlc99
BuildConfig
Katabasis

View File

@ -35,76 +35,64 @@
#include "FileSystem.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QDebug>
#include <QUrl>
#include <QSaveFile>
#include <QStandardPaths>
#include <QTextStream>
#include <QUrl>
#if defined Q_OS_WIN32
#include <windows.h>
#include <string>
#include <sys/utime.h>
#include <winnls.h>
#include <shobjidl.h>
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
#include <shlobj.h>
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <sys/utime.h>
#include <windows.h>
#include <winnls.h>
#include <string>
#else
#include <utime.h>
#include <utime.h>
#endif
namespace FS {
void ensureExists(const QDir &dir)
void ensureExists(const QDir& dir)
{
if (!QDir().mkpath(dir.absolutePath()))
{
throw FileSystemException("Unable to create folder " + dir.dirName() + " (" +
dir.absolutePath() + ")");
if (!QDir().mkpath(dir.absolutePath())) {
throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")");
}
}
void write(const QString &filename, const QByteArray &data)
void write(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QSaveFile file(filename);
if (!file.open(QSaveFile::WriteOnly))
{
throw FileSystemException("Couldn't open " + filename + " for writing: " +
file.errorString());
if (!file.open(QSaveFile::WriteOnly)) {
throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
if (data.size() != file.write(data))
{
throw FileSystemException("Error writing data to " + filename + ": " +
file.errorString());
if (data.size() != file.write(data)) {
throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
if (!file.commit())
{
throw FileSystemException("Error while committing data to " + filename + ": " +
file.errorString());
if (!file.commit()) {
throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString());
}
}
QByteArray read(const QString &filename)
QByteArray read(const QString& filename)
{
QFile file(filename);
if (!file.open(QFile::ReadOnly))
{
throw FileSystemException("Unable to open " + filename + " for reading: " +
file.errorString());
if (!file.open(QFile::ReadOnly)) {
throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString());
}
const qint64 size = file.size();
QByteArray data(int(size), 0);
const qint64 ret = file.read(data.data(), size);
if (ret == -1 || ret != size)
{
throw FileSystemException("Error reading data from " + filename + ": " +
file.errorString());
if (ret == -1 || ret != size) {
throw FileSystemException("Error reading data from " + filename + ": " + file.errorString());
}
return data;
}
@ -138,12 +126,12 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
bool copy::operator()(const QString &offset)
bool copy::operator()(const QString& offset)
{
//NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
m_followSymlinks = true;
#endif
#endif
auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset);
@ -152,52 +140,39 @@ bool copy::operator()(const QString &offset)
if (!currentSrc.exists())
return false;
if(!m_followSymlinks && currentSrc.isSymLink())
{
if (!m_followSymlinks && currentSrc.isSymLink()) {
qDebug() << "creating symlink" << src << " - " << dst;
if (!ensureFilePathExists(dst))
{
if (!ensureFilePathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
return QFile::link(currentSrc.symLinkTarget(), dst);
}
else if(currentSrc.isFile())
{
} else if (currentSrc.isFile()) {
qDebug() << "copying file" << src << " - " << dst;
if (!ensureFilePathExists(dst))
{
if (!ensureFilePathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
return QFile::copy(src, dst);
}
else if(currentSrc.isDir())
{
} else if (currentSrc.isDir()) {
qDebug() << "recursing" << offset;
if (!ensureFolderPathExists(dst))
{
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))
{
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))
{
if (m_blacklist && m_blacklist->matches(inner_offset)) {
continue;
}
if(!operator()(inner_offset))
{
if (!operator()(inner_offset)) {
qWarning() << "Failed to copy" << inner_offset;
return false;
}
}
}
else
{
} else {
qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
return false;
}
@ -208,55 +183,41 @@ bool deletePath(QString path)
{
bool OK = true;
QFileInfo finfo(path);
if(finfo.isFile()) {
if (finfo.isFile()) {
return QFile::remove(path);
}
QDir dir(path);
if (!dir.exists())
{
if (!dir.exists()) {
return OK;
}
auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
QDir::AllDirs | QDir::Files,
QDir::DirsFirst);
auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst);
for(auto & info: allEntries)
{
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())
{
if (dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) {
if (info.isFile()) {
OK &= QFile::remove(info.absoluteFilePath());
}
else if (info.isDir())
{
} 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())
{
if (info.isSymLink()) {
OK &= QFile::remove(info.absoluteFilePath());
}
#endif
else if (info.isDir())
{
else if (info.isDir()) {
OK &= deletePath(info.absoluteFilePath());
}
else if (info.isFile())
{
} else if (info.isFile()) {
OK &= QFile::remove(info.absoluteFilePath());
}
else
{
} else {
OK = false;
qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath();
}
@ -265,22 +226,30 @@ bool deletePath(QString path)
return OK;
}
QString PathCombine(const QString & path1, const QString & path2)
bool trash(QString path, QString *pathInTrash = nullptr)
{
if(!path1.size())
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
return false;
#else
return QFile::moveToTrash(path, pathInTrash);
#endif
}
QString PathCombine(const QString& path1, const QString& path2)
{
if (!path1.size())
return path2;
if(!path2.size())
if (!path2.size())
return path1;
return QDir::cleanPath(path1 + QDir::separator() + path2);
}
QString PathCombine(const QString & path1, const QString & path2, const QString & path3)
QString PathCombine(const QString& path1, const QString& path2, const QString& path3)
{
return PathCombine(PathCombine(path1, path2), path3);
}
QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4)
QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4)
{
return PathCombine(PathCombine(path1, path2, path3), path4);
}
@ -292,17 +261,14 @@ QString AbsolutePath(QString path)
QString ResolveExecutable(QString path)
{
if (path.isEmpty())
{
if (path.isEmpty()) {
return QString();
}
if(!path.contains('/'))
{
if (!path.contains('/')) {
path = QStandardPaths::findExecutable(path);
}
QFileInfo pathInfo(path);
if(!pathInfo.exists() || !pathInfo.isExecutable())
{
if (!pathInfo.exists() || !pathInfo.isExecutable()) {
return QString();
}
return pathInfo.absoluteFilePath();
@ -322,12 +288,9 @@ QString NormalizePath(QString path)
QDir b(path);
QString newAbsolute = b.absolutePath();
if (newAbsolute.startsWith(currentAbsolute))
{
if (newAbsolute.startsWith(currentAbsolute)) {
return a.relativeFilePath(newAbsolute);
}
else
{
} else {
return newAbsolute;
}
}
@ -336,10 +299,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
{
if (badFilenameChars.contains(string[i]))
{
for (int i = 0; i < string.length(); i++) {
if (badFilenameChars.contains(string[i])) {
string[i] = replaceWith;
}
}
@ -351,15 +312,12 @@ QString DirNameFromString(QString string, QString inDir)
int num = 0;
QString baseName = RemoveInvalidFilenameChars(string, '-');
QString dirName;
do
{
if(num == 0)
{
do {
if (num == 0) {
dirName = baseName;
}
else
{
dirName = baseName + QString::number(num);;
} else {
dirName = baseName + QString::number(num);
;
}
// If it's over 9000
@ -383,36 +341,31 @@ bool checkProblemticPathJava(QDir folder)
bool called_coinit = false;
HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args)
HRESULT CreateLink(LPCCH linkPath, LPCWSTR targetPath, LPCWSTR args)
{
HRESULT hres;
if (!called_coinit)
{
if (!called_coinit) {
hres = CoInitialize(NULL);
called_coinit = true;
if (!SUCCEEDED(hres))
{
if (!SUCCEEDED(hres)) {
qWarning("Failed to initialize COM. Error 0x%08lX", hres);
return hres;
}
}
IShellLinkA *link;
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink,
(LPVOID *)&link);
IShellLink* link;
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&link);
if (SUCCEEDED(hres))
{
IPersistFile *persistFile;
if (SUCCEEDED(hres)) {
IPersistFile* persistFile;
link->SetPath(targetPath);
link->SetArguments(args);
hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile);
if (SUCCEEDED(hres))
{
hres = link->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile);
if (SUCCEEDED(hres)) {
WCHAR wstr[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH);
@ -433,8 +386,7 @@ QString getDesktopDir()
}
// Cross-platform Shortcut creation
bool createShortCut(QString location, QString dest, QStringList args, QString name,
QString icon)
bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon)
{
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
location = PathCombine(location, name + ".desktop");
@ -459,8 +411,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
stream.flush();
f.close();
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup |
QFileDevice::ExeOther);
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
return true;
#elif defined Q_OS_WIN

View File

@ -41,29 +41,27 @@
#include <QDir>
#include <QFlags>
namespace FS
{
namespace FS {
class FileSystemException : public ::Exception
{
public:
FileSystemException(const QString &message) : Exception(message) {}
class FileSystemException : public ::Exception {
public:
FileSystemException(const QString& message) : Exception(message) {}
};
/**
* write data to a file safely
*/
void write(const QString &filename, const QByteArray &data);
void write(const QString& filename, const QByteArray& data);
/**
* read data from a file safely\
*/
QByteArray read(const QString &filename);
QByteArray read(const QString& filename);
/**
* Update the last changed timestamp of an existing file
*/
bool updateTimestamp(const QString & filename);
bool updateTimestamp(const QString& filename);
/**
* Creates all the folders in a path for the specified path
@ -77,35 +75,31 @@ bool ensureFilePathExists(QString filenamepath);
*/
bool ensureFolderPathExists(QString filenamepath);
class copy
{
public:
copy(const QString & src, const QString & dst)
class copy {
public:
copy(const QString& src, const QString& dst)
{
m_src.setPath(src);
m_dst.setPath(dst);
}
copy & followSymlinks(const bool follow)
copy& followSymlinks(const bool follow)
{
m_followSymlinks = follow;
return *this;
}
copy & blacklist(const IPathMatcher * filter)
copy& blacklist(const IPathMatcher* filter)
{
m_blacklist = filter;
return *this;
}
bool operator()()
{
return operator()(QString());
}
bool operator()() { return operator()(QString()); }
private:
bool operator()(const QString &offset);
private:
bool operator()(const QString& offset);
private:
private:
bool m_followSymlinks = true;
const IPathMatcher * m_blacklist = nullptr;
const IPathMatcher* m_blacklist = nullptr;
QDir m_src;
QDir m_dst;
};
@ -115,9 +109,14 @@ private:
*/
bool deletePath(QString path);
QString PathCombine(const QString &path1, const QString &path2);
QString PathCombine(const QString &path1, const QString &path2, const QString &path3);
QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4);
/**
* Trash a folder / file
*/
bool trash(QString path, QString *pathInTrash);
QString PathCombine(const QString& path1, const QString& path2);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4);
QString AbsolutePath(QString path);

View File

@ -44,7 +44,7 @@
#include "QObjectPtr.h"
#include "modplatform/flame/PackManifest.h"
#include <nonstd/optional>
#include <optional>
class QuaZip;
namespace Flame
@ -90,8 +90,8 @@ private: /* data */
QString m_archivePath;
bool m_downloadRequired = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods;
enum class ModpackType{
Unknown,

View File

@ -33,30 +33,32 @@
* limitations under the License.
*/
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QSet>
#include <QFile>
#include <QThread>
#include <QTextStream>
#include <QXmlStreamReader>
#include <QTimer>
#include <QDebug>
#include <QFileSystemWatcher>
#include <QUuid>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeData>
#include <QSet>
#include <QStack>
#include <QPair>
#include <QTextStream>
#include <QThread>
#include <QTimer>
#include <QUuid>
#include <QXmlStreamReader>
#include "InstanceList.h"
#include "BaseInstance.h"
#include "InstanceTask.h"
#include "settings/INISettingsObject.h"
#include "NullInstance.h"
#include "minecraft/MinecraftInstance.h"
#include "FileSystem.h"
#include "ExponentialSeries.h"
#include "FileSystem.h"
#include "InstanceList.h"
#include "InstanceTask.h"
#include "NullInstance.h"
#include "WatchLock.h"
#include "minecraft/MinecraftInstance.h"
#include "settings/INISettingsObject.h"
#ifdef Q_OS_WIN32
#include <Windows.h>
@ -64,13 +66,12 @@
const static int GROUP_FILE_FORMAT_VERSION = 1;
InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent)
InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent)
: QAbstractListModel(parent), m_globalSettings(settings)
{
resumeWatch();
// Create aand normalize path
if (!QDir::current().exists(instDir))
{
if (!QDir::current().exists(instDir)) {
QDir::current().mkpath(instDir);
}
@ -83,9 +84,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir,
m_watcher->addPath(m_instDir);
}
InstanceList::~InstanceList()
{
}
InstanceList::~InstanceList() {}
Qt::DropActions InstanceList::supportedDragActions() const
{
@ -99,7 +98,7 @@ Qt::DropActions InstanceList::supportedDropActions() const
bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const
{
if(data && data->hasFormat("application/x-instanceid")) {
if (data && data->hasFormat("application/x-instanceid")) {
return true;
}
return false;
@ -107,7 +106,7 @@ bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action,
bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent)
{
if(data && data->hasFormat("application/x-instanceid")) {
if (data && data->hasFormat("application/x-instanceid")) {
return true;
}
return false;
@ -120,35 +119,33 @@ QStringList InstanceList::mimeTypes() const
return types;
}
QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const
QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
{
auto mimeData = QAbstractListModel::mimeData(indexes);
if(indexes.size() == 1) {
if (indexes.size() == 1) {
auto instanceId = data(indexes[0], InstanceIDRole).toString();
mimeData->setData("application/x-instanceid", instanceId.toUtf8());
}
return mimeData;
}
int InstanceList::rowCount(const QModelIndex &parent) const
int InstanceList::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return m_instances.count();
}
QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const
QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const
{
Q_UNUSED(parent);
if (row < 0 || row >= m_instances.size())
return QModelIndex();
return createIndex(row, column, (void *)m_instances.at(row).get());
return createIndex(row, column, (void*)m_instances.at(row).get());
}
QVariant InstanceList::data(const QModelIndex &index, int role) const
QVariant InstanceList::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
{
if (!index.isValid()) {
return QVariant();
}
BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
@ -193,29 +190,25 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const
bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid())
{
if (!index.isValid()) {
return false;
}
if(role != Qt::EditRole)
{
if (role != Qt::EditRole) {
return false;
}
BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer());
auto newName = value.toString();
if(pdata->name() == newName)
{
if (pdata->name() == newName) {
return true;
}
pdata->setName(newName);
return true;
}
Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const
{
Qt::ItemFlags f;
if (index.isValid())
{
if (index.isValid()) {
f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
}
return f;
@ -224,13 +217,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
{
auto inst = getInstanceById(id);
if(!inst)
{
if (!inst) {
return GroupId();
}
auto iter = m_instanceGroupIndex.find(inst->id());
if(iter != m_instanceGroupIndex.end())
{
if (iter != m_instanceGroupIndex.end()) {
return *iter;
}
return GroupId();
@ -239,33 +230,27 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
{
auto inst = getInstanceById(id);
if(!inst)
{
if (!inst) {
qDebug() << "Attempt to set a null instance's group";
return;
}
bool changed = false;
auto iter = m_instanceGroupIndex.find(inst->id());
if(iter != m_instanceGroupIndex.end())
{
if(*iter != name)
{
if (iter != m_instanceGroupIndex.end()) {
if (*iter != name) {
*iter = name;
changed = true;
}
}
else
{
} else {
changed = true;
m_instanceGroupIndex[id] = name;
}
if(changed)
{
if (changed) {
m_groupNameCache.insert(name);
auto idx = getInstIndex(inst.get());
emit dataChanged(index(idx), index(idx), {GroupRole});
emit dataChanged(index(idx), index(idx), { GroupRole });
saveGroupList();
}
}
@ -279,24 +264,20 @@ void InstanceList::deleteGroup(const QString& name)
{
bool removed = false;
qDebug() << "Delete group" << name;
for(auto & instance: m_instances)
{
const auto & instID = instance->id();
for (auto& instance : m_instances) {
const auto& instID = instance->id();
auto instGroupName = getInstanceGroup(instID);
if(instGroupName == name)
{
if (instGroupName == name) {
m_instanceGroupIndex.remove(instID);
qDebug() << "Remove" << instID << "from group" << name;
removed = true;
auto idx = getInstIndex(instance.get());
if(idx > 0)
{
emit dataChanged(index(idx), index(idx), {GroupRole});
if (idx > 0) {
emit dataChanged(index(idx), index(idx), { GroupRole });
}
}
}
if(removed)
{
if (removed) {
saveGroupList();
}
}
@ -306,23 +287,75 @@ bool InstanceList::isGroupCollapsed(const QString& group)
return m_collapsedGroups.contains(group);
}
bool InstanceList::trashInstance(const InstanceId& id)
{
auto inst = getInstanceById(id);
if (!inst) {
qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?).";
return false;
}
auto cachedGroupId = m_instanceGroupIndex[id];
qDebug() << "Will trash instance" << id;
QString trashedLoc;
if (m_instanceGroupIndex.remove(id)) {
saveGroupList();
}
if (!FS::trash(inst->instanceRoot(), &trashedLoc)) {
qDebug() << "Trash of instance" << id << "has not been completely successfully...";
return false;
}
qDebug() << "Instance" << id << "has been trashed by the launcher.";
m_trashHistory.push({id, inst->instanceRoot(), trashedLoc, cachedGroupId});
return true;
}
bool InstanceList::trashedSomething() {
return !m_trashHistory.empty();
}
void InstanceList::undoTrashInstance() {
if (m_trashHistory.empty()) {
qWarning() << "Nothing to recover from trash.";
return;
}
auto top = m_trashHistory.pop();
while (QDir(top.polyPath).exists()) {
top.id += "1";
top.polyPath += "1";
}
qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath;
QFile(top.trashPath).rename(top.polyPath);
m_instanceGroupIndex[top.id] = top.groupName;
m_groupNameCache.insert(top.groupName);
saveGroupList();
emit instancesChanged();
}
void InstanceList::deleteInstance(const InstanceId& id)
{
auto inst = getInstanceById(id);
if(!inst)
{
if (!inst) {
qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
return;
}
if(m_instanceGroupIndex.remove(id))
{
if (m_instanceGroupIndex.remove(id)) {
saveGroupList();
}
qDebug() << "Will delete instance" << id;
if(!FS::deletePath(inst->instanceRoot()))
{
if (!FS::deletePath(inst->instanceRoot())) {
qWarning() << "Deletion of instance" << id << "has not been completely successful ...";
return;
}
@ -330,15 +363,13 @@ void InstanceList::deleteInstance(const InstanceId& id)
qDebug() << "Instance" << id << "has been deleted by the launcher.";
}
static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list)
static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr>& list)
{
QMap<InstanceId, InstanceLocator> out;
int i = 0;
for(auto & item: list)
{
for (auto& item : list) {
auto id = item->id();
if(out.contains(id))
{
if (out.contains(id)) {
qWarning() << "Duplicate ID" << id << "in instance list";
}
out[id] = std::make_pair(item, i);
@ -347,24 +378,21 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &
return out;
}
QList< InstanceId > InstanceList::discoverInstances()
QList<InstanceId> InstanceList::discoverInstances()
{
qDebug() << "Discovering instances in" << m_instDir;
QList<InstanceId> out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
while (iter.hasNext())
{
while (iter.hasNext()) {
QString subDir = iter.next();
QFileInfo dirInfo(subDir);
if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
continue;
// if it is a symlink, ignore it if it goes to the instance folder
if(dirInfo.isSymLink())
{
if (dirInfo.isSymLink()) {
QFileInfo targetInfo(dirInfo.symLinkTarget());
QFileInfo instDirInfo(m_instDir);
if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath())
{
if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) {
qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
continue;
}
@ -388,74 +416,56 @@ InstanceList::InstListError InstanceList::loadList()
QList<InstancePtr> newList;
for(auto & id: discoverInstances())
{
if(existingIds.contains(id))
{
for (auto& id : discoverInstances()) {
if (existingIds.contains(id)) {
auto instPair = existingIds[id];
existingIds.remove(id);
qDebug() << "Should keep and soft-reload" << id;
}
else
{
} else {
InstancePtr instPtr = loadInstance(id);
if(instPtr)
{
if (instPtr) {
newList.append(instPtr);
}
}
}
// TODO: looks like a general algorithm with a few specifics inserted. Do something about it.
if(!existingIds.isEmpty())
{
if (!existingIds.isEmpty()) {
// get the list of removed instances and sort it by their original index, from last to first
auto deadList = existingIds.values();
auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool
{
return a.second > b.second;
};
auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; };
std::sort(deadList.begin(), deadList.end(), orderSortPredicate);
// remove the contiguous ranges of rows
int front_bookmark = -1;
int back_bookmark = -1;
int currentItem = -1;
auto removeNow = [&]()
{
auto removeNow = [&]() {
beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
endRemoveRows();
front_bookmark = -1;
back_bookmark = currentItem;
};
for(auto & removedItem: deadList)
{
for (auto& removedItem : deadList) {
auto instPtr = removedItem.first;
instPtr->invalidate();
currentItem = removedItem.second;
if(back_bookmark == -1)
{
if (back_bookmark == -1) {
// no bookmark yet
back_bookmark = currentItem;
}
else if(currentItem == front_bookmark - 1)
{
} else if (currentItem == front_bookmark - 1) {
// part of contiguous sequence, continue
}
else
{
} else {
// seam between previous and current item
removeNow();
}
front_bookmark = currentItem;
}
if(back_bookmark != -1)
{
if (back_bookmark != -1) {
removeNow();
}
}
if(newList.size())
{
if (newList.size()) {
add(newList);
}
m_dirty = false;
@ -466,26 +476,23 @@ InstanceList::InstListError InstanceList::loadList()
void InstanceList::updateTotalPlayTime()
{
totalPlayTime = 0;
for(auto const& itr : m_instances)
{
for (auto const& itr : m_instances) {
totalPlayTime += itr.get()->totalTimePlayed();
}
}
void InstanceList::saveNow()
{
for(auto & item: m_instances)
{
for (auto& item : m_instances) {
item->saveNow();
}
}
void InstanceList::add(const QList<InstancePtr> &t)
void InstanceList::add(const QList<InstancePtr>& t)
{
beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1);
m_instances.append(t);
for(auto & ptr : t)
{
for (auto& ptr : t) {
connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
}
endInsertRows();
@ -493,69 +500,61 @@ void InstanceList::add(const QList<InstancePtr> &t)
void InstanceList::resumeWatch()
{
if(m_watchLevel > 0)
{
if (m_watchLevel > 0) {
qWarning() << "Bad suspend level resume in instance list";
return;
}
m_watchLevel++;
if(m_watchLevel > 0 && m_dirty)
{
if (m_watchLevel > 0 && m_dirty) {
loadList();
}
}
void InstanceList::suspendWatch()
{
m_watchLevel --;
m_watchLevel--;
}
void InstanceList::providerUpdated()
{
m_dirty = true;
if(m_watchLevel == 1)
{
if (m_watchLevel == 1) {
loadList();
}
}
InstancePtr InstanceList::getInstanceById(QString instId) const
{
if(instId.isEmpty())
if (instId.isEmpty())
return InstancePtr();
for(auto & inst: m_instances)
{
if (inst->id() == instId)
{
for (auto& inst : m_instances) {
if (inst->id() == instId) {
return inst;
}
}
return InstancePtr();
}
QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
QModelIndex InstanceList::getInstanceIndexById(const QString& id) const
{
return index(getInstIndex(getInstanceById(id).get()));
}
int InstanceList::getInstIndex(BaseInstance *inst) const
int InstanceList::getInstIndex(BaseInstance* inst) const
{
int count = m_instances.count();
for (int i = 0; i < count; i++)
{
if (inst == m_instances[i].get())
{
for (int i = 0; i < count; i++) {
if (inst == m_instances[i].get()) {
return i;
}
}
return -1;
}
void InstanceList::propertiesChanged(BaseInstance *inst)
void InstanceList::propertiesChanged(BaseInstance* inst)
{
int i = getInstIndex(inst);
if (i != -1)
{
if (i != -1) {
emit dataChanged(index(i), index(i));
updateTotalPlayTime();
}
@ -563,8 +562,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst)
InstancePtr InstanceList::loadInstance(const InstanceId& id)
{
if(!m_groupsLoaded)
{
if (!m_groupsLoaded) {
loadGroupList();
}
@ -592,50 +590,42 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id)
void InstanceList::saveGroupList()
{
qDebug() << "Will save group list now.";
if(!m_instancesProbed)
{
if (!m_instancesProbed) {
qDebug() << "Group saving prevented because we don't know the full list of instances yet.";
return;
}
WatchLock foo(m_watcher, m_instDir);
QString groupFileName = m_instDir + "/instgroups.json";
QMap<QString, QSet<QString>> reverseGroupMap;
for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++)
{
for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) {
QString id = iter.key();
QString group = iter.value();
if (group.isEmpty())
continue;
if(!instanceSet.contains(id))
{
if (!instanceSet.contains(id)) {
qDebug() << "Skipping saving missing instance" << id << "to groups list.";
continue;
}
if (!reverseGroupMap.count(group))
{
if (!reverseGroupMap.count(group)) {
QSet<QString> set;
set.insert(id);
reverseGroupMap[group] = set;
}
else
{
QSet<QString> &set = reverseGroupMap[group];
} else {
QSet<QString>& set = reverseGroupMap[group];
set.insert(id);
}
}
QJsonObject toplevel;
toplevel.insert("formatVersion", QJsonValue(QString("1")));
QJsonObject groupsArr;
for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++)
{
for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) {
auto list = iter.value();
auto name = iter.key();
QJsonObject groupObj;
QJsonArray instanceArr;
groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
for (auto item : list)
{
for (auto item : list) {
instanceArr.append(QJsonValue(item));
}
groupObj.insert("instances", instanceArr);
@ -643,13 +633,10 @@ void InstanceList::saveGroupList()
}
toplevel.insert("groups", groupsArr);
QJsonDocument doc(toplevel);
try
{
try {
FS::write(groupFileName, doc.toJson());
qDebug() << "Group list saved.";
}
catch (const FS::FileSystemException &e)
{
} catch (const FS::FileSystemException& e) {
qCritical() << "Failed to write instance group file :" << e.cause();
}
}
@ -665,12 +652,9 @@ void InstanceList::loadGroupList()
return;
QByteArray jsonData;
try
{
try {
jsonData = FS::read(groupFileName);
}
catch (const FS::FileSystemException &e)
{
} catch (const FS::FileSystemException& e) {
qCritical() << "Failed to read instance group file :" << e.cause();
return;
}
@ -679,17 +663,15 @@ void InstanceList::loadGroupList()
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
// if the json was bad, fail
if (error.error != QJsonParseError::NoError)
{
if (error.error != QJsonParseError::NoError) {
qCritical() << QString("Failed to parse instance group file: %1 at offset %2")
.arg(error.errorString(), QString::number(error.offset))
.toUtf8();
.arg(error.errorString(), QString::number(error.offset))
.toUtf8();
return;
}
// if the root of the json wasn't an object, fail
if (!jsonDoc.isObject())
{
if (!jsonDoc.isObject()) {
qWarning() << "Invalid group file. Root entry should be an object.";
return;
}
@ -701,8 +683,7 @@ void InstanceList::loadGroupList()
return;
// Get the groups. if it's not an object, fail
if (!rootObj.value("groups").isObject())
{
if (!rootObj.value("groups").isObject()) {
qWarning() << "Invalid group list JSON: 'groups' should be an object.";
return;
}
@ -712,21 +693,20 @@ void InstanceList::loadGroupList()
// Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject();
for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++)
{
for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) {
QString groupName = iter.key();
// If not an object, complain and skip to the next one.
if (!iter.value().isObject())
{
if (!iter.value().isObject()) {
qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8();
continue;
}
QJsonObject groupObj = iter.value().toObject();
if (!groupObj.value("instances").isArray())
{
qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8();
if (!groupObj.value("instances").isArray()) {
qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.")
.arg(groupName)
.toUtf8();
continue;
}
@ -734,15 +714,14 @@ void InstanceList::loadGroupList()
groupSet.insert(groupName);
auto hidden = groupObj.value("hidden").toBool(false);
if(hidden) {
if (hidden) {
m_collapsedGroups.insert(groupName);
}
// Iterate through the list of instances in the group.
QJsonArray instancesArray = groupObj.value("instances").toArray();
for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++)
{
for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) {
m_instanceGroupIndex[(*iter2).toString()] = groupName;
}
}
@ -757,13 +736,11 @@ void InstanceList::instanceDirContentsChanged(const QString& path)
emit instancesChanged();
}
void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value)
{
QString newInstDir = QDir(value.toString()).canonicalPath();
if(newInstDir != m_instDir)
{
if(m_groupsLoaded)
{
if (newInstDir != m_instDir) {
if (m_groupsLoaded) {
saveGroupList();
}
m_instDir = newInstDir;
@ -775,7 +752,7 @@ void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
{
qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded");
if(collapsed) {
if (collapsed) {
m_collapsedGroups.insert(group);
} else {
m_collapsedGroups.remove(group);
@ -783,19 +760,14 @@ void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
saveGroupList();
}
class InstanceStaging : public Task
{
Q_OBJECT
class InstanceStaging : public Task {
Q_OBJECT
const unsigned minBackoff = 1;
const unsigned maxBackoff = 16;
public:
InstanceStaging (
InstanceList * parent,
Task * child,
const QString & stagingPath,
const QString& instanceName,
const QString& groupName )
: backoff(minBackoff, maxBackoff)
public:
InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName)
: backoff(minBackoff, maxBackoff)
{
m_parent = parent;
m_child.reset(child);
@ -810,62 +782,51 @@ public:
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
}
virtual ~InstanceStaging() {};
virtual ~InstanceStaging(){};
// FIXME/TODO: add ability to abort during instance commit retries
bool abort() override
{
if(m_child && m_child->canAbort())
{
if (m_child && m_child->canAbort()) {
return m_child->abort();
}
return false;
}
bool canAbort() const override
{
if(m_child && m_child->canAbort())
{
if (m_child && m_child->canAbort()) {
return true;
}
return false;
}
protected:
virtual void executeTask() override
{
m_child->start();
}
QStringList warnings() const override
{
return m_child->warnings();
}
protected:
virtual void executeTask() override { m_child->start(); }
QStringList warnings() const override { return m_child->warnings(); }
private slots:
private slots:
void childSucceded()
{
unsigned sleepTime = backoff();
if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName))
{
if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) {
emitSucceeded();
return;
}
// we actually failed, retry?
if(sleepTime == maxBackoff)
{
if (sleepTime == maxBackoff) {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return;
}
qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500);
}
void childFailed(const QString & reason)
void childFailed(const QString& reason)
{
m_parent->destroyStagingPath(m_stagingPath);
emitFailed(reason);
}
private:
private:
/*
* 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
@ -873,14 +834,14 @@ private:
*/
ExponentialSeries backoff;
QString m_stagingPath;
InstanceList * m_parent;
InstanceList* m_parent;
unique_qobject_ptr<Task> m_child;
QString m_instanceName;
QString m_groupName;
QTimer m_backoffTimer;
};
Task * InstanceList::wrapInstanceTask(InstanceTask * task)
Task* InstanceList::wrapInstanceTask(InstanceTask* task)
{
auto stagingPath = getStagedInstancePath();
task->setStagingPath(stagingPath);
@ -895,8 +856,7 @@ QString InstanceList::getStagedInstancePath()
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);
auto path = FS::PathCombine(m_instDir, relPath);
if(!rootPath.mkpath(relPath))
{
if (!rootPath.mkpath(relPath)) {
return QString();
}
#ifdef Q_OS_WIN32
@ -913,8 +873,7 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst
{
WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID);
if(!dir.rename(path, destination))
{
if (!dir.rename(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination;
return false;
}
@ -933,7 +892,8 @@ bool InstanceList::destroyStagingPath(const QString& keyPath)
return FS::deletePath(keyPath);
}
int InstanceList::getTotalPlayTime() {
int InstanceList::getTotalPlayTime()
{
updateTotalPlayTime();
return totalPlayTime;
}

View File

@ -19,6 +19,8 @@
#include <QAbstractListModel>
#include <QSet>
#include <QList>
#include <QStack>
#include <QPair>
#include "BaseInstance.h"
@ -46,6 +48,12 @@ enum class GroupsState
Dirty
};
struct TrashHistoryItem {
QString id;
QString polyPath;
QString trashPath;
QString groupName;
};
class InstanceList : public QAbstractListModel
{
@ -102,6 +110,9 @@ public:
void setInstanceGroup(const InstanceId & id, const GroupId& name);
void deleteGroup(const GroupId & name);
bool trashInstance(const InstanceId &id);
bool trashedSomething();
void undoTrashInstance();
void deleteInstance(const InstanceId & id);
// Wrap an instance creation task in some more task machinery and make it ready to be used
@ -180,4 +191,6 @@ private:
QSet<InstanceId> instanceSet;
bool m_groupsLoaded = false;
bool m_instancesProbed = false;
QStack<TrashHistoryItem> m_trashHistory;
};

View File

@ -18,13 +18,17 @@ LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@
LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")"
echo "Launcher Dir: ${LAUNCHER_DIR}"
# Set up env - filter out input LD_ variables but pass them in under different names
export GAME_LIBRARY_PATH=${GAME_LIBRARY_PATH-${LD_LIBRARY_PATH}}
export GAME_PRELOAD=${GAME_PRELOAD-${LD_PRELOAD}}
export LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@":$LAUNCHER_LIBRARY_PATH
export LD_PRELOAD=$LAUNCHER_PRELOAD
export QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins"
export QT_FONTPATH="${LAUNCHER_DIR}/fonts"
# Set up env.
# Pass our custom variables separately so that the launcher can remove them for child processes
export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@"
export LAUNCHER_LD_PRELOAD=""
export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins"
export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts"
export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH"
export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD"
export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH"
export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH"
# Detect missing dependencies...
DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'`

View File

@ -141,9 +141,10 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
QSet<QString> addedFiles;
// Modify the jar
for (auto i = mods.constEnd(); i != mods.constBegin(); --i)
// This needs to be done in reverse-order to ensure we respect the loading order of components
for (auto i = mods.crbegin(); i != mods.crend(); i++)
{
const Mod* mod = *i;
const auto* mod = *i;
// do not merge disabled mods.
if (!mod->enabled())
continue;
@ -267,7 +268,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours
nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
{
QDir directory(target);
QStringList extracted;
@ -276,7 +277,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
auto numEntries = zip->getEntriesCount();
if(numEntries < 0) {
qWarning() << "Failed to enumerate files in archive";
return nonstd::nullopt;
return std::nullopt;
}
else if(numEntries == 0) {
qDebug() << "Extracting empty archives seems odd...";
@ -285,7 +286,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
else if (!zip->goToFirstFile())
{
qWarning() << "Failed to seek to first file in zip";
return nonstd::nullopt;
return std::nullopt;
}
do
@ -322,7 +323,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
{
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath;
JlCompress::removeFile(extracted);
return nonstd::nullopt;
return std::nullopt;
}
extracted.append(absFilePath);
@ -340,7 +341,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar
}
// ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
@ -351,13 +352,13 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
return nonstd::nullopt;
return std::nullopt;
}
return MMCZip::extractSubDir(&zip, "", dir);
}
// ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
@ -368,7 +369,7 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
return nonstd::nullopt;
return std::nullopt;
}
return MMCZip::extractSubDir(&zip, subdir, dir);
}

View File

@ -42,7 +42,7 @@
#include <functional>
#include <quazip/JlCompress.h>
#include <nonstd/optional>
#include <optional>
namespace MMCZip
{
@ -95,7 +95,7 @@ namespace MMCZip
/**
* Extract a subdirectory from an archive
*/
nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
@ -106,7 +106,7 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir);
std::optional<QStringList> extractDir(QString fileCompressed, QString dir);
/**
* Extract a subdirectory from an archive
@ -116,7 +116,7 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
/**
* Extract a single file from an archive into a directory

View File

@ -77,10 +77,12 @@ public:
{
return m_ptr;
}
bool operator==(const shared_qobject_ptr<T>& other) {
template<typename U>
bool operator==(const shared_qobject_ptr<U>& other) const {
return m_ptr == other.m_ptr;
}
bool operator!=(const shared_qobject_ptr<T>& other) {
template<typename U>
bool operator!=(const shared_qobject_ptr<U>& other) const {
return m_ptr != other.m_ptr;
}

View File

@ -52,26 +52,25 @@ JavaUtils::JavaUtils()
{
}
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH)
QString stripVariableEntries(QString name, QString target, QString remove)
{
QDir mmcBin(QCoreApplication::applicationDirPath());
auto items = LD_LIBRARY_PATH.split(':');
QStringList final;
for(auto & item: items)
{
QDir test(item);
if(test == mmcBin)
{
qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item;
continue;
}
final.append(item);
}
return final.join(':');
}
char delimiter = ':';
#ifdef Q_OS_WIN32
delimiter = ';';
#endif
auto targetItems = target.split(delimiter);
auto toRemove = remove.split(delimiter);
for (QString item : toRemove) {
bool removed = targetItems.removeOne(item);
if (!removed)
qWarning() << "Entry" << item
<< "could not be stripped from variable" << name;
}
return targetItems.join(delimiter);
}
QProcessEnvironment CleanEnviroment()
{
// prepare the process environment
@ -89,6 +88,16 @@ QProcessEnvironment CleanEnviroment()
"JAVA_OPTIONS",
"JAVA_TOOL_OPTIONS"
};
QStringList stripped =
{
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
"LD_LIBRARY_PATH",
"LD_PRELOAD",
#endif
"QT_PLUGIN_PATH",
"QT_FONTPATH"
};
for(auto key: rawenv.keys())
{
auto value = rawenv.value(key);
@ -98,19 +107,22 @@ QProcessEnvironment CleanEnviroment()
qDebug() << "Env: ignoring" << key << value;
continue;
}
// filter PolyMC-related things
if(key.startsWith("QT_"))
// These are used to strip the original variables
// If there is "LD_LIBRARY_PATH" and "LAUNCHER_LD_LIBRARY_PATH", we want to
// remove all values in "LAUNCHER_LD_LIBRARY_PATH" from "LD_LIBRARY_PATH"
if(key.startsWith("LAUNCHER_"))
{
qDebug() << "Env: ignoring" << key << value;
continue;
}
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// Do not pass LD_* variables to java. They were intended for PolyMC
if(key.startsWith("LD_"))
if(stripped.contains(key))
{
qDebug() << "Env: ignoring" << key << value;
continue;
QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key));
qDebug() << "Env: stripped" << key << value << "to" << newValue;
}
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
// Strip IBus
// IBus is a Linux IME framework. For some reason, it breaks MC?
if (key == "XMODIFIERS" && value.contains(IBUS))
@ -119,22 +131,12 @@ QProcessEnvironment CleanEnviroment()
value.replace(IBUS, "");
qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value;
}
if(key == "GAME_PRELOAD")
{
env.insert("LD_PRELOAD", value);
continue;
}
if(key == "GAME_LIBRARY_PATH")
{
env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value));
continue;
}
#endif
// qDebug() << "Env: " << key << value;
env.insert(key, value);
}
#ifdef Q_OS_LINUX
// HACK: Workaround for QTBUG42500
// HACK: Workaround for QTBUG-42500
if(!env.contains("LD_LIBRARY_PATH"))
{
env.insert("LD_LIBRARY_PATH", "");
@ -203,7 +205,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
// Read the current type version from the registry.
// This will be used to find any key that contains the JavaHome value.
TCHAR subKeyName[255];
WCHAR subKeyName[255];
DWORD subKeyNameSize, numSubKeys, retCode;
// Get the number of subkeys
@ -229,12 +231,11 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS)
{
// Read the JavaHome value to find where Java is installed.
TCHAR *value = NULL;
DWORD valueSz = 0;
if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value,
&valueSz) == ERROR_MORE_DATA)
if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL,
&valueSz) == ERROR_SUCCESS)
{
value = new TCHAR[valueSz];
WCHAR *value = new WCHAR[valueSz];
RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value,
&valueSz);

View File

@ -24,6 +24,7 @@
#include <windows.h>
#endif
QString stripVariableEntries(QString name, QString target, QString remove);
QProcessEnvironment CleanEnviroment();
class JavaUtils : public QObject

View File

@ -282,35 +282,22 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
void LaunchTask::substituteVariables(const QStringList &args) const
void LaunchTask::substituteVariables(QStringList &args) const
{
auto variables = m_instance->getVariables();
auto envVariables = QProcessEnvironment::systemEnvironment();
auto env = m_instance->createEnvironment();
for (auto arg : args) {
for (auto key : variables)
{
arg.replace("$" + key, variables.value(key));
}
for (auto env : envVariables.keys())
{
arg.replace("$" + env, envVariables.value(env));
}
for (auto key : env.keys())
{
args.replaceInStrings("$" + key, env.value(key));
}
}
QString LaunchTask::substituteVariables(const QString &cmd) const
void LaunchTask::substituteVariables(QString &cmd) const
{
QString out = cmd;
auto variables = m_instance->getVariables();
for (auto it = variables.begin(); it != variables.end(); ++it)
auto env = m_instance->createEnvironment();
for (auto key : env.keys())
{
out.replace("$" + it.key(), it.value());
cmd.replace("$" + key, env.value(key));
}
auto env = QProcessEnvironment::systemEnvironment();
for (auto var : env.keys())
{
out.replace("$" + var, env.value(var));
}
return out;
}

View File

@ -105,8 +105,8 @@ public: /* methods */
shared_qobject_ptr<LogModel> getLogModel();
public:
void substituteVariables(const QStringList &args) const;
QString substituteVariables(const QString &cmd) const;
void substituteVariables(QStringList &args) const;
void substituteVariables(QString &cmd) const;
QString censorPrivateInfo(QString in);
protected: /* methods */

View File

@ -56,9 +56,10 @@ void PostLaunchCommand::executeTask()
const QString program = args.takeFirst();
m_process.start(program, args);
#else
QString postlaunch_cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(postlaunch_cmd), MessageLevel::Launcher);
m_process.start(postlaunch_cmd);
m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
#endif
}

View File

@ -56,9 +56,10 @@ void PreLaunchCommand::executeTask()
const QString program = args.takeFirst();
m_process.start(program, args);
#else
QString prelaunch_cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd), MessageLevel::Launcher);
m_process.start(prelaunch_cmd);
m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
#endif
}

View File

@ -709,15 +709,15 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{
if(mod->type() == Mod::MOD_FOLDER)
{
out << u8" [📁] " + mod->fileinfo().completeBaseName() + " (folder)";
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue;
}
if(mod->enabled()) {
out << u8" [✔]" + mod->fileinfo().completeBaseName();
out << u8" [✔] " + mod->fileinfo().completeBaseName();
}
else {
out << u8" [] " + mod->fileinfo().completeBaseName() + " (disabled)";
out << u8" [] " + mod->fileinfo().completeBaseName() + " (disabled)";
}
}

View File

@ -53,12 +53,12 @@
#include <QCoreApplication>
#include <nonstd/optional>
#include <optional>
using nonstd::optional;
using nonstd::nullopt;
using std::optional;
using std::nullopt;
GameType::GameType(nonstd::optional<int> original):
GameType::GameType(std::optional<int> original):
original(original)
{
if(!original) {

View File

@ -16,11 +16,11 @@
#pragma once
#include <QFileInfo>
#include <QDateTime>
#include <nonstd/optional>
#include <optional>
struct GameType {
GameType() = default;
GameType (nonstd::optional<int> original);
GameType (std::optional<int> original);
QString toTranslatedString() const;
QString toLogString() const;
@ -33,7 +33,7 @@ struct GameType {
Adventure,
Spectator
} type = Unknown;
nonstd::optional<int> original;
std::optional<int> original;
};
class World

View File

@ -109,8 +109,10 @@ QStringList AccountList::profileNames() const {
void AccountList::addAccount(const MinecraftAccountPtr account)
{
// NOTE: Do not allow adding something that's already there
if(m_accounts.contains(account)) {
// NOTE: Do not allow adding something that's already there. We shouldn't let it continue
// because of the signal / slot connections after this.
if (m_accounts.contains(account)) {
qDebug() << "Tried to add account that's already on the accounts list!";
return;
}
@ -123,6 +125,8 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
if(profileId.size()) {
auto existingAccount = findAccountByProfileId(profileId);
if(existingAccount != -1) {
qDebug() << "Replacing old account with a new one with the same profile ID!";
MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount];
m_accounts[existingAccount] = account;
if(m_defaultAccount == existingAccountPtr) {
@ -138,9 +142,12 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
// if we don't have this profileId yet, add the account to the end
int row = m_accounts.count();
qDebug() << "Inserting account at index" << row;
beginInsertRows(QModelIndex(), row, row);
m_accounts.append(account);
endInsertRows();
onListChanged();
}

View File

@ -5,6 +5,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/AccountTask.h"
#include "net/NetUtils.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {
@ -58,10 +59,18 @@ void LauncherLoginStep::onRequestDone(
#ifndef NDEBUG
qDebug() << data;
#endif
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
);
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
);
}
else {
emit finished(
AccountTaskState::STATE_OFFLINE,
tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
);
}
return;
}

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {
@ -64,10 +65,18 @@ void MinecraftProfileStep::onRequestDone(
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed.")
);
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
);
}
else {
emit finished(
AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
);
}
return;
}
if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {
@ -67,10 +68,18 @@ void MinecraftProfileStepMojang::onRequestDone(
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed.")
);
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
);
}
else {
emit finished(
AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
);
}
return;
}
if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {

View File

@ -6,6 +6,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind):
AuthStep(data),
@ -62,10 +63,24 @@ void XboxAuthorizationStep::onRequestDone(
#endif
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
if(!processSTSError(error, data, headers)) {
if (Net::isApplicationError(error)) {
if(!processSTSError(error, data, headers)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error)
);
}
else {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)
);
}
}
else {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error)
AccountTaskState::STATE_OFFLINE,
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)
);
}
return;

View File

@ -6,6 +6,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {
@ -58,10 +59,18 @@ void XboxProfileStep::onRequestDone(
#ifndef NDEBUG
qDebug() << data;
#endif
finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to retrieve the Xbox profile.")
);
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)
);
}
else {
emit finished(
AccountTaskState::STATE_OFFLINE,
tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)
);
}
return;
}

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {
@ -53,7 +54,17 @@ void XboxUserStep::onRequestDone(
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed."));
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("XBox user authentication failed: %1").arg(requestor->errorString_)
);
}
else {
emit finished(
AccountTaskState::STATE_OFFLINE,
tr("XBox user authentication failed: %1").arg(requestor->errorString_)
);
}
return;
}

View File

@ -46,7 +46,7 @@
#include "minecraft/PackProfile.h"
#include "meta/Version.h"
#include <nonstd/optional>
#include <optional>
namespace ATLauncher {
@ -131,8 +131,8 @@ private:
Meta::VersionPtr minecraftVersion;
QMap<QString, Meta::VersionPtr> componentsToInstall;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QFuture<bool> m_modExtractFuture;
QFutureWatcher<bool> m_modExtractFutureWatcher;

View File

@ -7,6 +7,13 @@ Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAcc
: m_network(network), m_toProcess(toProcess)
{}
bool Flame::FileResolvingTask::abort()
{
if (m_dljob)
return m_dljob->abort();
return true;
}
void Flame::FileResolvingTask::executeTask()
{
setStatus(tr("Resolving mod IDs..."));

View File

@ -13,6 +13,9 @@ public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
bool canAbort() const override { return true; }
bool abort() override;
const Flame::Manifest &getResults() const
{
return m_toProcess;

View File

@ -123,7 +123,7 @@ void FlameCheckUpdate::executeTask()
continue;
}
setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod->name()));
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders });
@ -145,7 +145,7 @@ void FlameCheckUpdate::executeTask()
if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) {
auto pack = getProjectInfo(latest_ver);
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString());
emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url);
emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url);
continue;
}

View File

@ -10,7 +10,7 @@
#include "net/NetJob.h"
#include <nonstd/optional>
#include <optional>
namespace LegacyFTB {
@ -46,8 +46,8 @@ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
NetJob::Ptr netJobContainer;
QString archivePath;

View File

@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* 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
@ -40,103 +42,190 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/PackManifest.h"
#include "net/ChecksumValidator.h"
#include "settings/INISettingsObject.h"
#include "BuildConfig.h"
#include "Application.h"
#include "BuildConfig.h"
#include "ui/dialogs/ScrollMessageBox.h"
namespace ModpacksCH {
PackInstallTask::PackInstallTask(Modpack pack, QString version)
{
m_pack = pack;
m_version_name = version;
}
PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent)
: m_pack(std::move(pack)), m_version_name(std::move(version)), m_parent(parent)
{}
bool PackInstallTask::abort()
{
if(abortable)
{
return jobPtr->abort();
}
return false;
bool aborted = true;
if (m_net_job)
aborted &= m_net_job->abort();
if (m_mod_id_resolver_task)
aborted &= m_mod_id_resolver_task->abort();
// FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet...
if (aborted)
emitFailed(tr("Aborted"));
return aborted;
}
void PackInstallTask::executeTask()
{
setStatus(tr("Getting the manifest..."));
// Find pack version
bool found = false;
VersionInfo version;
auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(),
[this](ModpacksCH::VersionInfo const& a) { return a.name == m_version_name; });
for(auto vInfo : m_pack.versions) {
if (vInfo.name == m_version_name) {
found = true;
version = vInfo;
break;
}
}
if(!found) {
if (version_it == m_pack.versions.constEnd()) {
emitFailed(tr("Failed to find pack version %1").arg(m_version_name));
return;
}
auto *netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
auto version = *version_it;
QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response));
QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded);
QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed);
QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress);
m_net_job = netJob;
netJob->start();
}
void PackInstallTask::onDownloadSucceeded()
void PackInstallTask::onManifestDownloadSucceeded()
{
jobPtr.reset();
m_net_job.reset();
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << m_response;
return;
}
auto obj = doc.object();
ModpacksCH::Version version;
try
{
try {
auto obj = Json::requireObject(doc);
ModpacksCH::loadVersion(version, obj);
}
catch (const JSONValidationError &e)
{
} catch (const JSONValidationError& e) {
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
return;
}
m_version = version;
downloadPack();
resolveMods();
}
void PackInstallTask::onDownloadFailed(QString reason)
void PackInstallTask::resolveMods()
{
jobPtr.reset();
emitFailed(reason);
setStatus(tr("Resolving mods..."));
setProgress(0, 100);
m_file_id_map.clear();
Flame::Manifest manifest;
int index = 0;
for (auto const& file : m_version.files) {
if (!file.serverOnly && file.url.isEmpty()) {
if (file.curseforge.file_id <= 0) {
emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name));
return;
}
Flame::File flame_file;
flame_file.projectId = file.curseforge.project_id;
flame_file.fileId = file.curseforge.file_id;
flame_file.hash = file.sha1;
manifest.files.insert(flame_file.fileId, flame_file);
m_file_id_map.append(flame_file.fileId);
} else {
m_file_id_map.append(-1);
}
index++;
}
m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest);
connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded);
connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed);
connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress);
m_mod_id_resolver_task->start();
}
void PackInstallTask::onResolveModsSucceeded()
{
m_abortable = false;
QString text;
auto anyBlocked = false;
Flame::Manifest results = m_mod_id_resolver_task->getResults();
for (int index = 0; index < m_file_id_map.size(); index++) {
auto const file_id = m_file_id_map.at(index);
if (file_id < 0)
continue;
Flame::File results_file = results.files[file_id];
VersionFile& local_file = m_version.files[index];
// First check for blocked mods
if (!results_file.resolved || results_file.url.isEmpty()) {
QString type(local_file.type);
type[0] = type[0].toUpper();
text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl);
anyBlocked = true;
} else {
local_file.url = results_file.url.toString();
}
}
m_mod_id_resolver_task.reset();
if (anyBlocked) {
qDebug() << "Blocked files found, displaying file list";
auto message_dialog = new ScrollMessageBox(m_parent, tr("Blocked files found"),
tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."),
text);
if (message_dialog->exec() == QDialog::Accepted)
downloadPack();
else
abort();
} else {
downloadPack();
}
}
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading mods..."));
jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
for(auto file : m_version.files) {
if(file.serverOnly) continue;
auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
for (auto const& file : m_version.files) {
if (file.serverOnly || file.url.isEmpty())
continue;
QFileInfo fileName(file.name);
auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix();
QFileInfo file_info(file.name);
auto cacheName = file_info.completeBaseName() + "-" + file.sha1 + "." + file_info.suffix();
auto entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", cacheName);
entry->setStale(true);
@ -144,58 +233,64 @@ void PackInstallTask::downloadPack()
auto relpath = FS::PathCombine("minecraft", file.path, file.name);
auto path = FS::PathCombine(m_stagingPath, relpath);
if (filesToCopy.contains(path)) {
if (m_files_to_copy.contains(path)) {
qWarning() << "Ignoring" << file.url << "as a file of that path is already downloading.";
continue;
}
qDebug() << "Will download" << file.url << "to" << path;
filesToCopy[path] = entry->getFullPath();
m_files_to_copy[path] = entry->getFullPath();
auto dl = Net::Download::makeCached(file.url, entry);
if (!file.sha1.isEmpty()) {
auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
}
jobPtr->addNetAction(dl);
}
connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
{
abortable = false;
jobPtr.reset();
install();
});
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
{
abortable = false;
jobPtr.reset();
emitFailed(reason);
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
abortable = true;
setProgress(current, total);
});
connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded);
connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed);
connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress);
m_net_job = jobPtr;
jobPtr->start();
m_abortable = true;
}
void PackInstallTask::onModDownloadSucceeded()
{
m_net_job.reset();
install();
}
void PackInstallTask::install()
{
setStatus(tr("Copying modpack files"));
setStatus(tr("Copying modpack files..."));
setProgress(0, m_files_to_copy.size());
QCoreApplication::processEvents();
for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) {
auto &to = iter.key();
auto &from = iter.value();
m_abortable = false;
int i = 0;
for (auto iter = m_files_to_copy.constBegin(); iter != m_files_to_copy.constEnd(); iter++) {
auto& to = iter.key();
auto& from = iter.value();
FS::copy fileCopyOperation(from, to);
if(!fileCopyOperation()) {
if (!fileCopyOperation()) {
qWarning() << "Failed to copy" << from << "to" << to;
emitFailed(tr("Failed to copy files"));
return;
}
setProgress(i++, m_files_to_copy.size());
QCoreApplication::processEvents();
}
setStatus(tr("Installing modpack"));
setStatus(tr("Installing modpack..."));
QCoreApplication::processEvents();
auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
@ -205,20 +300,20 @@ void PackInstallTask::install()
auto components = instance.getPackProfile();
components->buildingFromScratch();
for(auto target : m_version.targets) {
if(target.type == "game" && target.name == "minecraft") {
for (auto target : m_version.targets) {
if (target.type == "game" && target.name == "minecraft") {
components->setComponentVersion("net.minecraft", target.version, true);
break;
}
}
for(auto target : m_version.targets) {
if(target.type != "modloader") continue;
for (auto target : m_version.targets) {
if (target.type != "modloader")
continue;
if(target.name == "forge") {
if (target.name == "forge") {
components->setComponentVersion("net.minecraftforge", target.version);
}
else if(target.name == "fabric") {
} else if (target.name == "fabric") {
components->setComponentVersion("net.fabricmc.fabric-loader", target.version);
}
}
@ -245,4 +340,20 @@ void PackInstallTask::install()
emitSucceeded();
}
void PackInstallTask::onManifestDownloadFailed(QString reason)
{
m_net_job.reset();
emitFailed(reason);
}
void PackInstallTask::onResolveModsFailed(QString reason)
{
m_net_job.reset();
emitFailed(reason);
}
void PackInstallTask::onModDownloadFailed(QString reason)
{
m_net_job.reset();
emitFailed(reason);
}
} // namespace ModpacksCH

View File

@ -1,18 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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
* 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.
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
*
* 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
@ -20,44 +40,60 @@
#include "FTBPackManifest.h"
#include "InstanceTask.h"
#include "QObjectPtr.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "net/NetJob.h"
#include <QWidget>
namespace ModpacksCH {
class PackInstallTask : public InstanceTask
class PackInstallTask final : public InstanceTask
{
Q_OBJECT
public:
explicit PackInstallTask(Modpack pack, QString version);
virtual ~PackInstallTask(){}
explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr);
~PackInstallTask() override = default;
bool canAbort() const override { return true; }
bool canAbort() const override { return m_abortable; }
bool abort() override;
protected:
virtual void executeTask() override;
void executeTask() override;
private slots:
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
void onManifestDownloadSucceeded();
void onResolveModsSucceeded();
void onModDownloadSucceeded();
void onManifestDownloadFailed(QString reason);
void onResolveModsFailed(QString reason);
void onModDownloadFailed(QString reason);
private:
void resolveMods();
void downloadPack();
void install();
private:
bool abortable = false;
bool m_abortable = true;
NetJob::Ptr jobPtr;
QByteArray response;
NetJob::Ptr m_net_job = nullptr;
shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver_task = nullptr;
QList<int> m_file_id_map;
QByteArray m_response;
Modpack m_pack;
QString m_version_name;
Version m_version;
QMap<QString, QString> filesToCopy;
QMap<QString, QString> m_files_to_copy;
//FIXME: nuke
QWidget* m_parent;
};
}

View File

@ -1,18 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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
* 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.
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* 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 2020 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "FTBPackManifest.h"
@ -127,13 +146,16 @@ static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj)
a.path = Json::requireString(obj, "path");
a.name = Json::requireString(obj, "name");
a.version = Json::requireString(obj, "version");
a.url = Json::requireString(obj, "url");
a.url = Json::ensureString(obj, "url"); // optional
a.sha1 = Json::requireString(obj, "sha1");
a.size = Json::requireInteger(obj, "size");
a.clientOnly = Json::requireBoolean(obj, "clientonly");
a.serverOnly = Json::requireBoolean(obj, "serveronly");
a.optional = Json::requireBoolean(obj, "optional");
a.updated = Json::requireInteger(obj, "updated");
auto curseforgeObj = Json::ensureObject(obj, "curseforge"); // optional
a.curseforge.project_id = Json::ensureInteger(curseforgeObj, "project");
a.curseforge.file_id = Json::ensureInteger(curseforgeObj, "file");
}
void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj)

View File

@ -1,18 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020 Petr Mrazek <peterix@gmail.com>
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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
* 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.
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020 Petr Mrazek <peterix@gmail.com>
*
* 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
@ -97,6 +116,12 @@ struct VersionTarget
int64_t updated;
};
struct VersionFileCurseForge
{
int project_id;
int file_id;
};
struct VersionFile
{
int id;
@ -111,6 +136,7 @@ struct VersionFile
bool serverOnly;
bool optional;
int64_t updated;
VersionFileCurseForge curseforge;
};
struct Version

View File

@ -24,7 +24,7 @@
#include <QStringList>
#include <QUrl>
#include <nonstd/optional>
#include <optional>
namespace Technic {
@ -57,8 +57,8 @@ private:
QString m_archivePath;
NetJob::Ptr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
};
} // namespace Technic

44
launcher/net/NetUtils.h Normal file
View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* 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 <QNetworkReply>
#include <QSet>
namespace Net {
inline bool isApplicationError(QNetworkReply::NetworkError x) {
// Mainly taken from https://github.com/qt/qtbase/blob/dev/src/network/access/qhttpthreaddelegate.cpp
static QSet<QNetworkReply::NetworkError> errors = {
QNetworkReply::ProtocolInvalidOperationError,
QNetworkReply::AuthenticationRequiredError,
QNetworkReply::ContentAccessDenied,
QNetworkReply::ContentNotFoundError,
QNetworkReply::ContentOperationNotPermittedError,
QNetworkReply::ProxyAuthenticationRequiredError,
QNetworkReply::ContentConflictError,
QNetworkReply::ContentGoneError,
QNetworkReply::InternalServerError,
QNetworkReply::OperationNotImplementedError,
QNetworkReply::ServiceUnavailableError,
QNetworkReply::UnknownServerError,
QNetworkReply::UnknownContentError
};
return errors.contains(x);
}
} // namespace Net

View File

@ -252,6 +252,9 @@ public:
TranslatedAction actionViewInstanceFolder;
TranslatedAction actionViewCentralModsFolder;
QMenu * editMenu = nullptr;
TranslatedAction actionUndoTrashInstance;
QMenu * helpMenu = nullptr;
TranslatedToolButton helpMenuButton;
TranslatedAction actionReportBug;
@ -335,6 +338,14 @@ public:
actionSettings->setShortcut(QKeySequence::Preferences);
all_actions.append(&actionSettings);
actionUndoTrashInstance = TranslatedAction(MainWindow);
connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), MainWindow, SLOT(undoTrashInstance()));
actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance"));
actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion"));
actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
actionUndoTrashInstance->setShortcut(QKeySequence("Ctrl+Z"));
all_actions.append(&actionUndoTrashInstance);
if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
actionReportBug = TranslatedAction(MainWindow);
actionReportBug->setObjectName(QStringLiteral("actionReportBug"));
@ -508,6 +519,9 @@ public:
fileMenu->addSeparator();
fileMenu->addAction(actionSettings);
editMenu = menuBar->addMenu(tr("&Edit"));
editMenu->addAction(actionUndoTrashInstance);
viewMenu = menuBar->addMenu(tr("&View"));
viewMenu->setSeparatorsCollapsible(false);
viewMenu->addAction(actionCAT);
@ -732,9 +746,10 @@ public:
actionDeleteInstance = TranslatedAction(MainWindow);
actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance"));
actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance..."));
actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance"));
actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance."));
actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete});
actionDeleteInstance->setAutoRepeat(false);
all_actions.append(&actionDeleteInstance);
actionCopyInstance = TranslatedAction(MainWindow);
@ -1150,6 +1165,11 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup()));
actions.append(actionDeleteGroup);
}
QAction *actionUndoTrashInstance = new QAction("Undo last trash instance", this);
connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), SLOT(undoTrashInstance()));
actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
actions.append(actionUndoTrashInstance);
}
QMenu myMenu;
myMenu.addActions(actions);
@ -1832,6 +1852,11 @@ void MainWindow::deleteGroup()
}
}
void MainWindow::undoTrashInstance()
{
APPLICATION->instances()->undoTrashInstance();
}
void MainWindow::on_actionViewInstanceFolder_triggered()
{
QString str = APPLICATION->settings()->get("InstanceDir").toString();
@ -1957,7 +1982,12 @@ void MainWindow::on_actionDeleteInstance_triggered()
{
return;
}
auto id = m_selectedInstance->id();
if (APPLICATION->instances()->trashInstance(id)) {
return;
}
auto response = CustomMessageBox::selectable(
this,
tr("CAREFUL!"),

View File

@ -145,6 +145,7 @@ private slots:
void on_actionDeleteInstance_triggered();
void deleteGroup();
void undoTrashInstance();
void on_actionExportInstance_triggered();

View File

@ -158,8 +158,9 @@ void ModUpdateDialog::checkCandidates()
if (!reason.isEmpty())
text += tr("Reason: %1").arg(reason) + "<br>";
if (!recover_url.isEmpty())
text += tr("Possible solution: ") + tr("Getting the latest version manually:") + "<br>" +
QString("<a href='%1'>").arg(recover_url.toString()) + recover_url.toString() + "</a><br>";
//: %1 is the link to download it manually
text += tr("Possible solution: Getting the latest version manually:<br>%1<br>")
.arg(QString("<a href='%1'>%1</a>").arg(recover_url.toString()));
text += "<br>";
}
@ -241,9 +242,9 @@ auto ModUpdateDialog::ensureMetadata() -> bool
}
ChooseProviderDialog chooser(this);
chooser.setDescription(tr("This mod (%1) does not have a metadata yet. We need to create one in order to keep relevant "
"information on how to update this "
"mod. To do this, please select a mod provider from which we can search for updates for %1.")
chooser.setDescription(tr("The mod '%1' does not have a metadata yet. We need to generate it in order to track relevant "
"information on how to update this mod. "
"To do this, please select a mod provider which we can use to check for updates for this mod.")
.arg(candidate->name()));
auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted;
@ -330,7 +331,7 @@ void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::P
m_second_try_metadata->addTask(task);
} else {
QString reason{ tr("Didn't find a valid version on the selected mod provider(s)") };
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
m_failed_metadata.append({mod, reason});
}

View File

@ -43,8 +43,8 @@ void ProgressDialog::setSkipButton(bool present, QString label)
void ProgressDialog::on_skipButton_clicked(bool checked)
{
Q_UNUSED(checked);
task->abort();
QDialog::reject();
if (task->abort())
QDialog::reject();
}
ProgressDialog::~ProgressDialog()

View File

@ -32,13 +32,13 @@ class SortProxy : public QSortFilterProxyModel {
const auto& mod = model->at(source_row);
if (mod.name().contains(filterRegularExpression()))
if (filterRegularExpression().match(mod.name()).hasMatch())
return true;
if (mod.description().contains(filterRegularExpression()))
if (filterRegularExpression().match(mod.description()).hasMatch())
return true;
for (auto& author : mod.authors()) {
if (author.contains(filterRegularExpression())) {
if (filterRegularExpression().match(author).hasMatch()) {
return true;
}
}
@ -182,7 +182,7 @@ void ExternalResourcesPage::retranslate()
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
{
m_viewFilter = newContents;
m_filterModel->setFilterFixedString(m_viewFilter);
m_filterModel->setFilterRegularExpression(m_viewFilter);
}
void ExternalResourcesPage::runningStateChanged(bool running)

View File

@ -155,7 +155,7 @@
<string>Check for &amp;Updates</string>
</property>
<property name="toolTip">
<string>"Tries to find / update all selected resources (all resources if none is selected)"</string>
<string>Try to check or update all selected resources (all resources if none are selected)</string>
</property>
</action>
</widget>

View File

@ -349,7 +349,7 @@ void InstanceSettingsPage::loadSettings()
ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
#if !defined(Q_OS_LINUX)
ui->perfomanceGroupBox->setVisible(false);
ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false);
#endif
// Miscellanous

View File

@ -80,7 +80,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods);
ui->actionUpdateItem->setToolTip(tr("Tries to find / update all selected mods (all mods if none is selected)"));
ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)"));
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
@ -190,10 +190,15 @@ void ModFolderPage::updateMods()
return;
}
if (update_dialog.noUpdates()) {
CustomMessageBox::selectable(this, tr("Update checker"),
(mods_list.size() == 1)
? tr("'%1' is up-to-date! :)").arg(mods_list.front()->name())
: tr("All %1mods are up-to-date! :)").arg(use_all ? "" : (tr("selected") + " ")))
QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) };
if (mods_list.size() > 1) {
if (use_all) {
message = tr("All mods are up-to-date! :)");
} else {
message = tr("All selected mods are up-to-date! :)");
}
}
CustomMessageBox::selectable(this, tr("Update checker"), message)
->exec();
return;
}

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* 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
@ -126,7 +127,7 @@ void FtbPage::suggestCurrent()
return;
}
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion));
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this));
for(auto art : selected.art) {
if(art.type == "square") {
QString editedLogoName;

View File

@ -31,6 +31,7 @@ QPalette DarkTheme::colorScheme()
darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
darkPalette.setColor(QPalette::HighlightedText, Qt::black);
darkPalette.setColor(QPalette::PlaceholderText, Qt::darkGray);
return fadeInactive(darkPalette, fadeAmount(), fadeColor());
}