NOISSUE squish.
This commit is contained in:
parent
9e7cdbfe11
commit
9965decd81
@ -16,8 +16,6 @@ set(CORE_SOURCES
|
|||||||
LoggedProcess.cpp
|
LoggedProcess.cpp
|
||||||
MessageLevel.cpp
|
MessageLevel.cpp
|
||||||
MessageLevel.h
|
MessageLevel.h
|
||||||
FolderInstanceProvider.h
|
|
||||||
FolderInstanceProvider.cpp
|
|
||||||
BaseVersion.h
|
BaseVersion.h
|
||||||
BaseInstance.h
|
BaseInstance.h
|
||||||
BaseInstance.cpp
|
BaseInstance.cpp
|
||||||
|
@ -1,429 +0,0 @@
|
|||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "settings/INISettingsObject.h"
|
|
||||||
#include "FileSystem.h"
|
|
||||||
#include "minecraft/MinecraftInstance.h"
|
|
||||||
#include "minecraft/legacy/LegacyInstance.h"
|
|
||||||
#include "NullInstance.h"
|
|
||||||
#include "ExponentialSeries.h"
|
|
||||||
#include "WatchLock.h"
|
|
||||||
|
|
||||||
|
|
||||||
#include <QDir>
|
|
||||||
#include <QDirIterator>
|
|
||||||
#include <QFileSystemWatcher>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QUuid>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
const static int GROUP_FILE_FORMAT_VERSION = 1;
|
|
||||||
|
|
||||||
FolderInstanceProvider::FolderInstanceProvider(SettingsObjectPtr settings, const QString& instDir)
|
|
||||||
: m_globalSettings(settings)
|
|
||||||
{
|
|
||||||
// Create aand normalize path
|
|
||||||
if (!QDir::current().exists(instDir))
|
|
||||||
{
|
|
||||||
QDir::current().mkpath(instDir);
|
|
||||||
}
|
|
||||||
// NOTE: canonicalPath requires the path to exist. Do not move this above the creation block!
|
|
||||||
m_instDir = QDir(instDir).canonicalPath();
|
|
||||||
m_watcher = new QFileSystemWatcher(this);
|
|
||||||
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &FolderInstanceProvider::instanceDirContentsChanged);
|
|
||||||
m_watcher->addPath(m_instDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
QList< InstanceId > FolderInstanceProvider::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())
|
|
||||||
{
|
|
||||||
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())
|
|
||||||
{
|
|
||||||
QFileInfo targetInfo(dirInfo.symLinkTarget());
|
|
||||||
QFileInfo instDirInfo(m_instDir);
|
|
||||||
if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath())
|
|
||||||
{
|
|
||||||
qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto id = dirInfo.fileName();
|
|
||||||
out.append(id);
|
|
||||||
qDebug() << "Found instance ID" << id;
|
|
||||||
}
|
|
||||||
instanceSet = out.toSet();
|
|
||||||
m_instancesProbed = true;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
InstancePtr FolderInstanceProvider::loadInstance(const InstanceId& id)
|
|
||||||
{
|
|
||||||
if(!m_groupsLoaded)
|
|
||||||
{
|
|
||||||
loadGroupList();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto instanceRoot = FS::PathCombine(m_instDir, id);
|
|
||||||
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg"));
|
|
||||||
InstancePtr inst;
|
|
||||||
|
|
||||||
instanceSettings->registerSetting("InstanceType", "Legacy");
|
|
||||||
|
|
||||||
QString inst_type = instanceSettings->get("InstanceType").toString();
|
|
||||||
|
|
||||||
if (inst_type == "OneSix" || inst_type == "Nostalgia")
|
|
||||||
{
|
|
||||||
inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
|
|
||||||
}
|
|
||||||
else if (inst_type == "Legacy")
|
|
||||||
{
|
|
||||||
inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instanceRoot));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot));
|
|
||||||
}
|
|
||||||
inst->init();
|
|
||||||
auto iter = groupMap.find(id);
|
|
||||||
if (iter != groupMap.end())
|
|
||||||
{
|
|
||||||
inst->setGroupInitial((*iter));
|
|
||||||
}
|
|
||||||
connect(inst.get(), &BaseInstance::groupChanged, this, &FolderInstanceProvider::groupChanged);
|
|
||||||
qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot();
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FolderInstanceProvider::saveGroupList()
|
|
||||||
{
|
|
||||||
qDebug() << "Will save group list now.";
|
|
||||||
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 = groupMap.begin(); iter != groupMap.end(); iter++)
|
|
||||||
{
|
|
||||||
QString id = iter.key();
|
|
||||||
QString group = iter.value();
|
|
||||||
if (group.isEmpty())
|
|
||||||
continue;
|
|
||||||
if(!instanceSet.contains(id))
|
|
||||||
{
|
|
||||||
qDebug() << "Skipping saving missing instance" << id << "to groups list.";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reverseGroupMap.count(group))
|
|
||||||
{
|
|
||||||
QSet<QString> set;
|
|
||||||
set.insert(id);
|
|
||||||
reverseGroupMap[group] = set;
|
|
||||||
}
|
|
||||||
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++)
|
|
||||||
{
|
|
||||||
auto list = iter.value();
|
|
||||||
auto name = iter.key();
|
|
||||||
QJsonObject groupObj;
|
|
||||||
QJsonArray instanceArr;
|
|
||||||
groupObj.insert("hidden", QJsonValue(QString("false")));
|
|
||||||
for (auto item : list)
|
|
||||||
{
|
|
||||||
instanceArr.append(QJsonValue(item));
|
|
||||||
}
|
|
||||||
groupObj.insert("instances", instanceArr);
|
|
||||||
groupsArr.insert(name, groupObj);
|
|
||||||
}
|
|
||||||
toplevel.insert("groups", groupsArr);
|
|
||||||
QJsonDocument doc(toplevel);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
FS::write(groupFileName, doc.toJson());
|
|
||||||
qDebug() << "Group list saved.";
|
|
||||||
}
|
|
||||||
catch (const FS::FileSystemException &e)
|
|
||||||
{
|
|
||||||
qCritical() << "Failed to write instance group file :" << e.cause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FolderInstanceProvider::loadGroupList()
|
|
||||||
{
|
|
||||||
qDebug() << "Will load group list now.";
|
|
||||||
QSet<QString> groupSet;
|
|
||||||
|
|
||||||
QString groupFileName = m_instDir + "/instgroups.json";
|
|
||||||
|
|
||||||
// if there's no group file, fail
|
|
||||||
if (!QFileInfo(groupFileName).exists())
|
|
||||||
return;
|
|
||||||
|
|
||||||
QByteArray jsonData;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
jsonData = FS::read(groupFileName);
|
|
||||||
}
|
|
||||||
catch (const FS::FileSystemException &e)
|
|
||||||
{
|
|
||||||
qCritical() << "Failed to read instance group file :" << e.cause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonParseError error;
|
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
|
|
||||||
|
|
||||||
// if the json was bad, fail
|
|
||||||
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();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the root of the json wasn't an object, fail
|
|
||||||
if (!jsonDoc.isObject())
|
|
||||||
{
|
|
||||||
qWarning() << "Invalid group file. Root entry should be an object.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject rootObj = jsonDoc.object();
|
|
||||||
|
|
||||||
// Make sure the format version matches, otherwise fail.
|
|
||||||
if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Get the groups. if it's not an object, fail
|
|
||||||
if (!rootObj.value("groups").isObject())
|
|
||||||
{
|
|
||||||
qWarning() << "Invalid group list JSON: 'groups' should be an object.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
groupMap.clear();
|
|
||||||
|
|
||||||
// Iterate through all the groups.
|
|
||||||
QJsonObject groupMapping = rootObj.value("groups").toObject();
|
|
||||||
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())
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep a list/set of groups for choosing
|
|
||||||
groupSet.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++)
|
|
||||||
{
|
|
||||||
groupMap[(*iter2).toString()] = groupName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_groupsLoaded = true;
|
|
||||||
emit groupsChanged(groupSet);
|
|
||||||
qDebug() << "Group list loaded.";
|
|
||||||
}
|
|
||||||
|
|
||||||
void FolderInstanceProvider::groupChanged()
|
|
||||||
{
|
|
||||||
// save the groups. save all of them.
|
|
||||||
auto instance = (BaseInstance *) QObject::sender();
|
|
||||||
auto id = instance->id();
|
|
||||||
groupMap[id] = instance->group();
|
|
||||||
emit groupsChanged({instance->group()});
|
|
||||||
saveGroupList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void FolderInstanceProvider::instanceDirContentsChanged(const QString& path)
|
|
||||||
{
|
|
||||||
Q_UNUSED(path);
|
|
||||||
emit instancesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FolderInstanceProvider::on_InstFolderChanged(const Setting &setting, QVariant value)
|
|
||||||
{
|
|
||||||
QString newInstDir = QDir(value.toString()).canonicalPath();
|
|
||||||
if(newInstDir != m_instDir)
|
|
||||||
{
|
|
||||||
if(m_groupsLoaded)
|
|
||||||
{
|
|
||||||
saveGroupList();
|
|
||||||
}
|
|
||||||
m_instDir = newInstDir;
|
|
||||||
m_groupsLoaded = false;
|
|
||||||
emit instancesChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InstanceStaging : public Task
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
const unsigned minBackoff = 1;
|
|
||||||
const unsigned maxBackoff = 16;
|
|
||||||
public:
|
|
||||||
InstanceStaging (
|
|
||||||
FolderInstanceProvider * parent,
|
|
||||||
Task * child,
|
|
||||||
const QString & stagingPath,
|
|
||||||
const QString& instanceName,
|
|
||||||
const QString& groupName )
|
|
||||||
: backoff(minBackoff, maxBackoff)
|
|
||||||
{
|
|
||||||
m_parent = parent;
|
|
||||||
m_child.reset(child);
|
|
||||||
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded);
|
|
||||||
connect(child, &Task::failed, this, &InstanceStaging::childFailed);
|
|
||||||
connect(child, &Task::status, this, &InstanceStaging::setStatus);
|
|
||||||
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
|
|
||||||
m_instanceName = instanceName;
|
|
||||||
m_groupName = groupName;
|
|
||||||
m_stagingPath = stagingPath;
|
|
||||||
m_backoffTimer.setSingleShot(true);
|
|
||||||
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual ~InstanceStaging() {};
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void executeTask() override
|
|
||||||
{
|
|
||||||
m_child->start();
|
|
||||||
}
|
|
||||||
QStringList warnings() const override
|
|
||||||
{
|
|
||||||
return m_child->warnings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void childSucceded()
|
|
||||||
{
|
|
||||||
unsigned sleepTime = backoff();
|
|
||||||
if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName))
|
|
||||||
{
|
|
||||||
emitSucceeded();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// we actually failed, retry?
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
m_parent->destroyStagingPath(m_stagingPath);
|
|
||||||
emitFailed(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
/*
|
|
||||||
* WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
|
|
||||||
* Basically, it starts messing things up while MultiMC is extracting/creating instances
|
|
||||||
* and causes that horrible failure that is NTFS to lock files in place because they are open.
|
|
||||||
*/
|
|
||||||
ExponentialSeries backoff;
|
|
||||||
QString m_stagingPath;
|
|
||||||
FolderInstanceProvider * m_parent;
|
|
||||||
unique_qobject_ptr<Task> m_child;
|
|
||||||
QString m_instanceName;
|
|
||||||
QString m_groupName;
|
|
||||||
QTimer m_backoffTimer;
|
|
||||||
};
|
|
||||||
|
|
||||||
#include "InstanceTask.h"
|
|
||||||
Task * FolderInstanceProvider::wrapInstanceTask(InstanceTask * task)
|
|
||||||
{
|
|
||||||
auto stagingPath = getStagedInstancePath();
|
|
||||||
task->setStagingPath(stagingPath);
|
|
||||||
task->setParentSettings(m_globalSettings);
|
|
||||||
return new InstanceStaging(this, task, stagingPath, task->name(), task->group());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FolderInstanceProvider::getStagedInstancePath()
|
|
||||||
{
|
|
||||||
QString key = QUuid::createUuid().toString();
|
|
||||||
QString relPath = FS::PathCombine("_MMC_TEMP/" , key);
|
|
||||||
QDir rootPath(m_instDir);
|
|
||||||
auto path = FS::PathCombine(m_instDir, relPath);
|
|
||||||
if(!rootPath.mkpath(relPath))
|
|
||||||
{
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FolderInstanceProvider::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName)
|
|
||||||
{
|
|
||||||
QDir dir;
|
|
||||||
QString instID = FS::DirNameFromString(instanceName, m_instDir);
|
|
||||||
{
|
|
||||||
WatchLock lock(m_watcher, m_instDir);
|
|
||||||
QString destination = FS::PathCombine(m_instDir, instID);
|
|
||||||
if(!dir.rename(path, destination))
|
|
||||||
{
|
|
||||||
qWarning() << "Failed to move" << path << "to" << destination;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
groupMap[instID] = groupName;
|
|
||||||
instanceSet.insert(instID);
|
|
||||||
emit groupsChanged({groupName});
|
|
||||||
emit instancesChanged();
|
|
||||||
}
|
|
||||||
saveGroupList();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FolderInstanceProvider::destroyStagingPath(const QString& keyPath)
|
|
||||||
{
|
|
||||||
return FS::deletePath(keyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "FolderInstanceProvider.moc"
|
|
@ -1,86 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QMap>
|
|
||||||
#include "BaseInstance.h"
|
|
||||||
#include "settings/SettingsObject.h"
|
|
||||||
|
|
||||||
#include "multimc_logic_export.h"
|
|
||||||
|
|
||||||
class QFileSystemWatcher;
|
|
||||||
class InstanceTask;
|
|
||||||
using InstanceId = QString;
|
|
||||||
using GroupId = QString;
|
|
||||||
using InstanceLocator = std::pair<InstancePtr, int>;
|
|
||||||
|
|
||||||
enum class InstCreateError
|
|
||||||
{
|
|
||||||
NoCreateError = 0,
|
|
||||||
NoSuchVersion,
|
|
||||||
UnknownCreateError,
|
|
||||||
InstExists,
|
|
||||||
CantCreateDir
|
|
||||||
};
|
|
||||||
|
|
||||||
class MULTIMC_LOGIC_EXPORT FolderInstanceProvider : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
FolderInstanceProvider(SettingsObjectPtr settings, const QString & instDir);
|
|
||||||
virtual ~FolderInstanceProvider() = default;
|
|
||||||
|
|
||||||
public:
|
|
||||||
/// used by InstanceList to @return a list of plausible IDs to probe for
|
|
||||||
QList<InstanceId> discoverInstances();
|
|
||||||
|
|
||||||
/// used by InstanceList to (re)load an instance with the given @id.
|
|
||||||
InstancePtr loadInstance(const InstanceId& id);
|
|
||||||
|
|
||||||
// Wrap an instance creation task in some more task machinery and make it ready to be used
|
|
||||||
Task * wrapInstanceTask(InstanceTask * task);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new empty staging area for instance creation and @return a path/key top commit it later.
|
|
||||||
* Used by instance manipulation tasks.
|
|
||||||
*/
|
|
||||||
QString getStagedInstancePath();
|
|
||||||
/**
|
|
||||||
* Commit the staging area given by @keyPath to the provider - used when creation succeeds.
|
|
||||||
* Used by instance manipulation tasks.
|
|
||||||
*/
|
|
||||||
bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName);
|
|
||||||
/**
|
|
||||||
* Destroy a previously created staging area given by @keyPath - used when creation fails.
|
|
||||||
* Used by instance manipulation tasks.
|
|
||||||
*/
|
|
||||||
bool destroyStagingPath(const QString & keyPath);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
// Emit this when the list of provided instances changed
|
|
||||||
void instancesChanged();
|
|
||||||
// Emit when the set of groups your provider supplies changes.
|
|
||||||
void groupsChanged(QSet<QString> groups);
|
|
||||||
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void on_InstFolderChanged(const Setting &setting, QVariant value);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void instanceDirContentsChanged(const QString &path);
|
|
||||||
void groupChanged();
|
|
||||||
|
|
||||||
private: /* methods */
|
|
||||||
void loadGroupList();
|
|
||||||
void saveGroupList();
|
|
||||||
|
|
||||||
private: /* data */
|
|
||||||
SettingsObjectPtr m_globalSettings;
|
|
||||||
QString m_instDir;
|
|
||||||
QFileSystemWatcher * m_watcher;
|
|
||||||
QMap<InstanceId, GroupId> groupMap;
|
|
||||||
QSet<InstanceId> instanceSet;
|
|
||||||
bool m_groupsLoaded = false;
|
|
||||||
bool m_instancesProbed = false;
|
|
||||||
};
|
|
@ -14,23 +14,50 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QDirIterator>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
|
#include <QTimer>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
#include "InstanceList.h"
|
#include "InstanceList.h"
|
||||||
#include "BaseInstance.h"
|
#include "BaseInstance.h"
|
||||||
|
#include "InstanceTask.h"
|
||||||
#include "FolderInstanceProvider.h"
|
#include "settings/INISettingsObject.h"
|
||||||
|
#include "minecraft/legacy/LegacyInstance.h"
|
||||||
|
#include "NullInstance.h"
|
||||||
|
#include "minecraft/MinecraftInstance.h"
|
||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
|
#include "ExponentialSeries.h"
|
||||||
|
#include "WatchLock.h"
|
||||||
|
|
||||||
InstanceList::InstanceList(QObject *parent)
|
const static int GROUP_FILE_FORMAT_VERSION = 1;
|
||||||
: QAbstractListModel(parent)
|
|
||||||
|
InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent)
|
||||||
|
: QAbstractListModel(parent), m_globalSettings(settings)
|
||||||
{
|
{
|
||||||
resumeWatch();
|
resumeWatch();
|
||||||
|
// Create aand normalize path
|
||||||
|
if (!QDir::current().exists(instDir))
|
||||||
|
{
|
||||||
|
QDir::current().mkpath(instDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated);
|
||||||
|
connect(this, &InstanceList::groupsChanged, this, &InstanceList::groupsPublished);
|
||||||
|
|
||||||
|
// NOTE: canonicalPath requires the path to exist. Do not move this above the creation block!
|
||||||
|
m_instDir = QDir(instDir).canonicalPath();
|
||||||
|
m_watcher = new QFileSystemWatcher(this);
|
||||||
|
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged);
|
||||||
|
m_watcher->addPath(m_instDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
InstanceList::~InstanceList()
|
InstanceList::~InstanceList()
|
||||||
@ -155,13 +182,44 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
QFileInfo targetInfo(dirInfo.symLinkTarget());
|
||||||
|
QFileInfo instDirInfo(m_instDir);
|
||||||
|
if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath())
|
||||||
|
{
|
||||||
|
qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto id = dirInfo.fileName();
|
||||||
|
out.append(id);
|
||||||
|
qDebug() << "Found instance ID" << id;
|
||||||
|
}
|
||||||
|
instanceSet = out.toSet();
|
||||||
|
m_instancesProbed = true;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
InstanceList::InstListError InstanceList::loadList()
|
InstanceList::InstListError InstanceList::loadList()
|
||||||
{
|
{
|
||||||
auto existingIds = getIdMapping(m_instances);
|
auto existingIds = getIdMapping(m_instances);
|
||||||
|
|
||||||
QList<InstancePtr> newList;
|
QList<InstancePtr> newList;
|
||||||
|
|
||||||
for(auto & id: m_provider->discoverInstances())
|
for(auto & id: discoverInstances())
|
||||||
{
|
{
|
||||||
if(existingIds.contains(id))
|
if(existingIds.contains(id))
|
||||||
{
|
{
|
||||||
@ -171,7 +229,7 @@ InstanceList::InstListError InstanceList::loadList()
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InstancePtr instPtr = m_provider->loadInstance(id);
|
InstancePtr instPtr = loadInstance(id);
|
||||||
if(instPtr)
|
if(instPtr)
|
||||||
{
|
{
|
||||||
newList.append(instPtr);
|
newList.append(instPtr);
|
||||||
@ -275,12 +333,6 @@ void InstanceList::suspendWatch()
|
|||||||
|
|
||||||
void InstanceList::providerUpdated()
|
void InstanceList::providerUpdated()
|
||||||
{
|
{
|
||||||
auto provider = dynamic_cast<FolderInstanceProvider *>(QObject::sender());
|
|
||||||
if(!provider)
|
|
||||||
{
|
|
||||||
qWarning() << "InstanceList::providerUpdated triggered by a non-provider";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_dirty = true;
|
m_dirty = true;
|
||||||
if(m_watchLevel == 1)
|
if(m_watchLevel == 1)
|
||||||
{
|
{
|
||||||
@ -293,13 +345,6 @@ void InstanceList::groupsPublished(QSet<QString> newGroups)
|
|||||||
m_groups.unite(newGroups);
|
m_groups.unite(newGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InstanceList::addInstanceProvider(FolderInstanceProvider* provider)
|
|
||||||
{
|
|
||||||
connect(provider, &FolderInstanceProvider::instancesChanged, this, &InstanceList::providerUpdated);
|
|
||||||
connect(provider, &FolderInstanceProvider::groupsChanged, this, &InstanceList::groupsPublished);
|
|
||||||
m_provider = provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
InstancePtr InstanceList::getInstanceById(QString instId) const
|
InstancePtr InstanceList::getInstanceById(QString instId) const
|
||||||
{
|
{
|
||||||
if(instId.isEmpty())
|
if(instId.isEmpty())
|
||||||
@ -340,3 +385,364 @@ void InstanceList::propertiesChanged(BaseInstance *inst)
|
|||||||
emit dataChanged(index(i), index(i));
|
emit dataChanged(index(i), index(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InstancePtr InstanceList::loadInstance(const InstanceId& id)
|
||||||
|
{
|
||||||
|
if(!m_groupsLoaded)
|
||||||
|
{
|
||||||
|
loadGroupList();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto instanceRoot = FS::PathCombine(m_instDir, id);
|
||||||
|
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg"));
|
||||||
|
InstancePtr inst;
|
||||||
|
|
||||||
|
instanceSettings->registerSetting("InstanceType", "Legacy");
|
||||||
|
|
||||||
|
QString inst_type = instanceSettings->get("InstanceType").toString();
|
||||||
|
|
||||||
|
if (inst_type == "OneSix" || inst_type == "Nostalgia")
|
||||||
|
{
|
||||||
|
inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
|
||||||
|
}
|
||||||
|
else if (inst_type == "Legacy")
|
||||||
|
{
|
||||||
|
inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instanceRoot));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot));
|
||||||
|
}
|
||||||
|
inst->init();
|
||||||
|
auto iter = groupMap.find(id);
|
||||||
|
if (iter != groupMap.end())
|
||||||
|
{
|
||||||
|
inst->setGroupInitial((*iter));
|
||||||
|
}
|
||||||
|
connect(inst.get(), &BaseInstance::groupChanged, this, &InstanceList::groupChanged);
|
||||||
|
qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot();
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstanceList::saveGroupList()
|
||||||
|
{
|
||||||
|
qDebug() << "Will save group list now.";
|
||||||
|
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 = groupMap.begin(); iter != groupMap.end(); iter++)
|
||||||
|
{
|
||||||
|
QString id = iter.key();
|
||||||
|
QString group = iter.value();
|
||||||
|
if (group.isEmpty())
|
||||||
|
continue;
|
||||||
|
if(!instanceSet.contains(id))
|
||||||
|
{
|
||||||
|
qDebug() << "Skipping saving missing instance" << id << "to groups list.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reverseGroupMap.count(group))
|
||||||
|
{
|
||||||
|
QSet<QString> set;
|
||||||
|
set.insert(id);
|
||||||
|
reverseGroupMap[group] = set;
|
||||||
|
}
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
auto list = iter.value();
|
||||||
|
auto name = iter.key();
|
||||||
|
QJsonObject groupObj;
|
||||||
|
QJsonArray instanceArr;
|
||||||
|
groupObj.insert("hidden", QJsonValue(QString("false")));
|
||||||
|
for (auto item : list)
|
||||||
|
{
|
||||||
|
instanceArr.append(QJsonValue(item));
|
||||||
|
}
|
||||||
|
groupObj.insert("instances", instanceArr);
|
||||||
|
groupsArr.insert(name, groupObj);
|
||||||
|
}
|
||||||
|
toplevel.insert("groups", groupsArr);
|
||||||
|
QJsonDocument doc(toplevel);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FS::write(groupFileName, doc.toJson());
|
||||||
|
qDebug() << "Group list saved.";
|
||||||
|
}
|
||||||
|
catch (const FS::FileSystemException &e)
|
||||||
|
{
|
||||||
|
qCritical() << "Failed to write instance group file :" << e.cause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstanceList::loadGroupList()
|
||||||
|
{
|
||||||
|
qDebug() << "Will load group list now.";
|
||||||
|
QSet<QString> groupSet;
|
||||||
|
|
||||||
|
QString groupFileName = m_instDir + "/instgroups.json";
|
||||||
|
|
||||||
|
// if there's no group file, fail
|
||||||
|
if (!QFileInfo(groupFileName).exists())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QByteArray jsonData;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsonData = FS::read(groupFileName);
|
||||||
|
}
|
||||||
|
catch (const FS::FileSystemException &e)
|
||||||
|
{
|
||||||
|
qCritical() << "Failed to read instance group file :" << e.cause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
|
||||||
|
|
||||||
|
// if the json was bad, fail
|
||||||
|
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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the root of the json wasn't an object, fail
|
||||||
|
if (!jsonDoc.isObject())
|
||||||
|
{
|
||||||
|
qWarning() << "Invalid group file. Root entry should be an object.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject rootObj = jsonDoc.object();
|
||||||
|
|
||||||
|
// Make sure the format version matches, otherwise fail.
|
||||||
|
if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get the groups. if it's not an object, fail
|
||||||
|
if (!rootObj.value("groups").isObject())
|
||||||
|
{
|
||||||
|
qWarning() << "Invalid group list JSON: 'groups' should be an object.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupMap.clear();
|
||||||
|
|
||||||
|
// Iterate through all the groups.
|
||||||
|
QJsonObject groupMapping = rootObj.value("groups").toObject();
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep a list/set of groups for choosing
|
||||||
|
groupSet.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++)
|
||||||
|
{
|
||||||
|
groupMap[(*iter2).toString()] = groupName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_groupsLoaded = true;
|
||||||
|
emit groupsChanged(groupSet);
|
||||||
|
qDebug() << "Group list loaded.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstanceList::groupChanged()
|
||||||
|
{
|
||||||
|
// save the groups. save all of them.
|
||||||
|
auto instance = (BaseInstance *) QObject::sender();
|
||||||
|
auto id = instance->id();
|
||||||
|
groupMap[id] = instance->group();
|
||||||
|
emit groupsChanged({instance->group()});
|
||||||
|
saveGroupList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void InstanceList::instanceDirContentsChanged(const QString& path)
|
||||||
|
{
|
||||||
|
Q_UNUSED(path);
|
||||||
|
emit instancesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
|
||||||
|
{
|
||||||
|
QString newInstDir = QDir(value.toString()).canonicalPath();
|
||||||
|
if(newInstDir != m_instDir)
|
||||||
|
{
|
||||||
|
if(m_groupsLoaded)
|
||||||
|
{
|
||||||
|
saveGroupList();
|
||||||
|
}
|
||||||
|
m_instDir = newInstDir;
|
||||||
|
m_groupsLoaded = false;
|
||||||
|
emit instancesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
m_parent = parent;
|
||||||
|
m_child.reset(child);
|
||||||
|
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded);
|
||||||
|
connect(child, &Task::failed, this, &InstanceStaging::childFailed);
|
||||||
|
connect(child, &Task::status, this, &InstanceStaging::setStatus);
|
||||||
|
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
|
||||||
|
m_instanceName = instanceName;
|
||||||
|
m_groupName = groupName;
|
||||||
|
m_stagingPath = stagingPath;
|
||||||
|
m_backoffTimer.setSingleShot(true);
|
||||||
|
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~InstanceStaging() {};
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void executeTask() override
|
||||||
|
{
|
||||||
|
m_child->start();
|
||||||
|
}
|
||||||
|
QStringList warnings() const override
|
||||||
|
{
|
||||||
|
return m_child->warnings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void childSucceded()
|
||||||
|
{
|
||||||
|
unsigned sleepTime = backoff();
|
||||||
|
if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName))
|
||||||
|
{
|
||||||
|
emitSucceeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// we actually failed, retry?
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
m_parent->destroyStagingPath(m_stagingPath);
|
||||||
|
emitFailed(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/*
|
||||||
|
* WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
|
||||||
|
* Basically, it starts messing things up while MultiMC is extracting/creating instances
|
||||||
|
* and causes that horrible failure that is NTFS to lock files in place because they are open.
|
||||||
|
*/
|
||||||
|
ExponentialSeries backoff;
|
||||||
|
QString m_stagingPath;
|
||||||
|
InstanceList * m_parent;
|
||||||
|
unique_qobject_ptr<Task> m_child;
|
||||||
|
QString m_instanceName;
|
||||||
|
QString m_groupName;
|
||||||
|
QTimer m_backoffTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
Task * InstanceList::wrapInstanceTask(InstanceTask * task)
|
||||||
|
{
|
||||||
|
auto stagingPath = getStagedInstancePath();
|
||||||
|
task->setStagingPath(stagingPath);
|
||||||
|
task->setParentSettings(m_globalSettings);
|
||||||
|
return new InstanceStaging(this, task, stagingPath, task->name(), task->group());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InstanceList::getStagedInstancePath()
|
||||||
|
{
|
||||||
|
QString key = QUuid::createUuid().toString();
|
||||||
|
QString relPath = FS::PathCombine("_MMC_TEMP/" , key);
|
||||||
|
QDir rootPath(m_instDir);
|
||||||
|
auto path = FS::PathCombine(m_instDir, relPath);
|
||||||
|
if(!rootPath.mkpath(relPath))
|
||||||
|
{
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName)
|
||||||
|
{
|
||||||
|
QDir dir;
|
||||||
|
QString instID = FS::DirNameFromString(instanceName, m_instDir);
|
||||||
|
{
|
||||||
|
WatchLock lock(m_watcher, m_instDir);
|
||||||
|
QString destination = FS::PathCombine(m_instDir, instID);
|
||||||
|
if(!dir.rename(path, destination))
|
||||||
|
{
|
||||||
|
qWarning() << "Failed to move" << path << "to" << destination;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
groupMap[instID] = groupName;
|
||||||
|
instanceSet.insert(instID);
|
||||||
|
emit groupsChanged({groupName});
|
||||||
|
emit instancesChanged();
|
||||||
|
}
|
||||||
|
saveGroupList();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InstanceList::destroyStagingPath(const QString& keyPath)
|
||||||
|
{
|
||||||
|
return FS::deletePath(keyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "InstanceList.moc"
|
@ -21,18 +21,33 @@
|
|||||||
#include <QList>
|
#include <QList>
|
||||||
|
|
||||||
#include "BaseInstance.h"
|
#include "BaseInstance.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
|
|
||||||
#include "multimc_logic_export.h"
|
#include "multimc_logic_export.h"
|
||||||
|
|
||||||
#include "QObjectPtr.h"
|
#include "QObjectPtr.h"
|
||||||
|
|
||||||
|
class QFileSystemWatcher;
|
||||||
|
class InstanceTask;
|
||||||
|
using InstanceId = QString;
|
||||||
|
using GroupId = QString;
|
||||||
|
using InstanceLocator = std::pair<InstancePtr, int>;
|
||||||
|
|
||||||
|
enum class InstCreateError
|
||||||
|
{
|
||||||
|
NoCreateError = 0,
|
||||||
|
NoSuchVersion,
|
||||||
|
UnknownCreateError,
|
||||||
|
InstExists,
|
||||||
|
CantCreateDir
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel
|
class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit InstanceList(QObject *parent = 0);
|
explicit InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent = 0);
|
||||||
virtual ~InstanceList();
|
virtual ~InstanceList();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -71,8 +86,6 @@ public:
|
|||||||
InstListError loadList();
|
InstListError loadList();
|
||||||
void saveNow();
|
void saveNow();
|
||||||
|
|
||||||
/// Add an instance provider. Takes ownership of it. Should only be done before the first load.
|
|
||||||
void addInstanceProvider(FolderInstanceProvider * provider);
|
|
||||||
|
|
||||||
InstancePtr getInstanceById(QString id) const;
|
InstancePtr getInstanceById(QString id) const;
|
||||||
QModelIndex getInstanceIndexById(const QString &id) const;
|
QModelIndex getInstanceIndexById(const QString &id) const;
|
||||||
@ -81,24 +94,63 @@ public:
|
|||||||
void deleteGroup(const GroupId & name);
|
void deleteGroup(const GroupId & name);
|
||||||
void deleteInstance(const InstanceId & id);
|
void deleteInstance(const InstanceId & id);
|
||||||
|
|
||||||
|
// Wrap an instance creation task in some more task machinery and make it ready to be used
|
||||||
|
Task * wrapInstanceTask(InstanceTask * task);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new empty staging area for instance creation and @return a path/key top commit it later.
|
||||||
|
* Used by instance manipulation tasks.
|
||||||
|
*/
|
||||||
|
QString getStagedInstancePath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit the staging area given by @keyPath to the provider - used when creation succeeds.
|
||||||
|
* Used by instance manipulation tasks.
|
||||||
|
*/
|
||||||
|
bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a previously created staging area given by @keyPath - used when creation fails.
|
||||||
|
* Used by instance manipulation tasks.
|
||||||
|
*/
|
||||||
|
bool destroyStagingPath(const QString & keyPath);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void dataIsInvalid();
|
void dataIsInvalid();
|
||||||
|
void instancesChanged();
|
||||||
|
void groupsChanged(QSet<QString> groups);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void on_InstFolderChanged(const Setting &setting, QVariant value);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void propertiesChanged(BaseInstance *inst);
|
void propertiesChanged(BaseInstance *inst);
|
||||||
void groupsPublished(QSet<QString>);
|
void groupsPublished(QSet<QString>);
|
||||||
void providerUpdated();
|
void providerUpdated();
|
||||||
|
void instanceDirContentsChanged(const QString &path);
|
||||||
|
void groupChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int getInstIndex(BaseInstance *inst) const;
|
int getInstIndex(BaseInstance *inst) const;
|
||||||
void suspendWatch();
|
void suspendWatch();
|
||||||
void resumeWatch();
|
void resumeWatch();
|
||||||
void add(const QList<InstancePtr> &list);
|
void add(const QList<InstancePtr> &list);
|
||||||
|
void loadGroupList();
|
||||||
|
void saveGroupList();
|
||||||
|
QList<InstanceId> discoverInstances();
|
||||||
|
InstancePtr loadInstance(const InstanceId& id);
|
||||||
|
|
||||||
protected:
|
private:
|
||||||
int m_watchLevel = 0;
|
int m_watchLevel = 0;
|
||||||
bool m_dirty = false;
|
bool m_dirty = false;
|
||||||
QList<InstancePtr> m_instances;
|
QList<InstancePtr> m_instances;
|
||||||
QSet<QString> m_groups;
|
QSet<QString> m_groups;
|
||||||
FolderInstanceProvider * m_provider;
|
|
||||||
|
SettingsObjectPtr m_globalSettings;
|
||||||
|
QString m_instDir;
|
||||||
|
QFileSystemWatcher * m_watcher;
|
||||||
|
QMap<InstanceId, GroupId> groupMap;
|
||||||
|
QSet<InstanceId> instanceSet;
|
||||||
|
bool m_groupsLoaded = false;
|
||||||
|
bool m_instancesProbed = false;
|
||||||
};
|
};
|
||||||
|
@ -86,7 +86,6 @@
|
|||||||
#include "dialogs/EditAccountDialog.h"
|
#include "dialogs/EditAccountDialog.h"
|
||||||
#include "dialogs/NotificationDialog.h"
|
#include "dialogs/NotificationDialog.h"
|
||||||
#include "dialogs/ExportInstanceDialog.h"
|
#include "dialogs/ExportInstanceDialog.h"
|
||||||
#include <FolderInstanceProvider.h>
|
|
||||||
#include <InstanceImportTask.h>
|
#include <InstanceImportTask.h>
|
||||||
#include "UpdateController.h"
|
#include "UpdateController.h"
|
||||||
#include "KonamiCode.h"
|
#include "KonamiCode.h"
|
||||||
@ -1279,11 +1278,8 @@ void MainWindow::runModalTask(Task *task)
|
|||||||
|
|
||||||
void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask)
|
void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask)
|
||||||
{
|
{
|
||||||
std::unique_ptr<Task> task(MMC->folderProvider()->wrapInstanceTask(rawTask));
|
std::unique_ptr<Task> task(MMC->instances()->wrapInstanceTask(rawTask));
|
||||||
runModalTask(task.get());
|
runModalTask(task.get());
|
||||||
|
|
||||||
// FIXME: handle instance selection after creation
|
|
||||||
// finalizeInstance(newInstance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_actionCopyInstance_triggered()
|
void MainWindow::on_actionCopyInstance_triggered()
|
||||||
@ -1299,11 +1295,8 @@ void MainWindow::on_actionCopyInstance_triggered()
|
|||||||
copyTask->setName(copyInstDlg.instName());
|
copyTask->setName(copyInstDlg.instName());
|
||||||
copyTask->setGroup(copyInstDlg.instGroup());
|
copyTask->setGroup(copyInstDlg.instGroup());
|
||||||
copyTask->setIcon(copyInstDlg.iconKey());
|
copyTask->setIcon(copyInstDlg.iconKey());
|
||||||
std::unique_ptr<Task> task(MMC->folderProvider()->wrapInstanceTask(copyTask));
|
std::unique_ptr<Task> task(MMC->instances()->wrapInstanceTask(copyTask));
|
||||||
runModalTask(task.get());
|
runModalTask(task.get());
|
||||||
|
|
||||||
// FIXME: handle instance selection after creation
|
|
||||||
// finalizeInstance(newInstance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::finalizeInstance(InstancePtr inst)
|
void MainWindow::finalizeInstance(InstancePtr inst)
|
||||||
|
@ -36,7 +36,6 @@
|
|||||||
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
#include "dialogs/CustomMessageBox.h"
|
||||||
#include "InstanceList.h"
|
#include "InstanceList.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
|
|
||||||
#include <minecraft/auth/MojangAccountList.h>
|
#include <minecraft/auth/MojangAccountList.h>
|
||||||
#include "icons/IconList.h"
|
#include "icons/IconList.h"
|
||||||
@ -597,10 +596,8 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
{
|
{
|
||||||
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems";
|
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems";
|
||||||
}
|
}
|
||||||
m_instances.reset(new InstanceList(this));
|
m_instances.reset(new InstanceList(m_settings, instDir, this));
|
||||||
m_instanceFolder = new FolderInstanceProvider(m_settings, instDir);
|
connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
|
||||||
connect(InstDirSetting.get(), &Setting::SettingChanged, m_instanceFolder, &FolderInstanceProvider::on_InstFolderChanged);
|
|
||||||
m_instances->addInstanceProvider(m_instanceFolder);
|
|
||||||
qDebug() << "Loading Instances...";
|
qDebug() << "Loading Instances...";
|
||||||
m_instances->loadList();
|
m_instances->loadList();
|
||||||
qDebug() << "<> Instances loaded.";
|
qDebug() << "<> Instances loaded.";
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
#include "LegacyUpgradePage.h"
|
#include "LegacyUpgradePage.h"
|
||||||
#include "ui_LegacyUpgradePage.h"
|
#include "ui_LegacyUpgradePage.h"
|
||||||
|
|
||||||
|
#include "InstanceList.h"
|
||||||
#include "minecraft/legacy/LegacyInstance.h"
|
#include "minecraft/legacy/LegacyInstance.h"
|
||||||
#include "minecraft/legacy/LegacyUpgradeTask.h"
|
#include "minecraft/legacy/LegacyUpgradeTask.h"
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
#include "dialogs/CustomMessageBox.h"
|
||||||
#include "dialogs/ProgressDialog.h"
|
#include "dialogs/ProgressDialog.h"
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ void LegacyUpgradePage::on_upgradeButton_clicked()
|
|||||||
upgradeTask->setName(newName);
|
upgradeTask->setName(newName);
|
||||||
upgradeTask->setGroup(m_inst->group());
|
upgradeTask->setGroup(m_inst->group());
|
||||||
upgradeTask->setIcon(m_inst->iconKey());
|
upgradeTask->setIcon(m_inst->iconKey());
|
||||||
std::unique_ptr<Task> task(MMC->folderProvider()->wrapInstanceTask(upgradeTask));
|
std::unique_ptr<Task> task(MMC->instances()->wrapInstanceTask(upgradeTask));
|
||||||
runModalTask(task.get());
|
runModalTask(task.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
#include "ui_FTBPage.h"
|
#include "ui_FTBPage.h"
|
||||||
|
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
#include "dialogs/CustomMessageBox.h"
|
||||||
#include "dialogs/NewInstanceDialog.h"
|
#include "dialogs/NewInstanceDialog.h"
|
||||||
#include "modplatform/ftb/FtbPackFetchTask.h"
|
#include "modplatform/ftb/FtbPackFetchTask.h"
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
#include "ui_ImportPage.h"
|
#include "ui_ImportPage.h"
|
||||||
|
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
|
||||||
#include "dialogs/ProgressDialog.h"
|
|
||||||
#include "dialogs/NewInstanceDialog.h"
|
#include "dialogs/NewInstanceDialog.h"
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QValidator>
|
#include <QValidator>
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
#include "ui_TechnicPage.h"
|
#include "ui_TechnicPage.h"
|
||||||
|
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
|
||||||
#include "dialogs/ProgressDialog.h"
|
|
||||||
#include "dialogs/NewInstanceDialog.h"
|
#include "dialogs/NewInstanceDialog.h"
|
||||||
|
|
||||||
TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
|
TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
#include "ui_TwitchPage.h"
|
#include "ui_TwitchPage.h"
|
||||||
|
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
|
||||||
#include "dialogs/ProgressDialog.h"
|
|
||||||
#include "dialogs/NewInstanceDialog.h"
|
#include "dialogs/NewInstanceDialog.h"
|
||||||
|
|
||||||
TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent)
|
TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent)
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
#include "ui_VanillaPage.h"
|
#include "ui_VanillaPage.h"
|
||||||
|
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "FolderInstanceProvider.h"
|
|
||||||
#include "dialogs/CustomMessageBox.h"
|
|
||||||
#include "dialogs/ProgressDialog.h"
|
|
||||||
|
|
||||||
#include <meta/Index.h>
|
#include <meta/Index.h>
|
||||||
#include <meta/VersionList.h>
|
#include <meta/VersionList.h>
|
||||||
|
Loading…
Reference in New Issue
Block a user