diff options
28 files changed, 1489 insertions, 987 deletions
diff --git a/api/gui/icons/IconList.cpp b/api/gui/icons/IconList.cpp index 4f9fb09f..b6be007f 100644 --- a/api/gui/icons/IconList.cpp +++ b/api/gui/icons/IconList.cpp @@ -243,7 +243,7 @@ int IconList::rowCount(const QModelIndex &parent) const return icons.size(); } -void IconList::installIcons(QStringList iconFiles) +void IconList::installIcons(const QStringList &iconFiles) { for (QString file : iconFiles) { @@ -261,7 +261,7 @@ void IconList::installIcons(QStringList iconFiles) } } -bool IconList::iconFileExists(QString key) +bool IconList::iconFileExists(const QString &key) const { auto iconEntry = icon(key); if(!iconEntry) @@ -271,7 +271,7 @@ bool IconList::iconFileExists(QString key) return iconEntry->has(IconType::FileBased); } -const MMCIcon *IconList::icon(QString key) +const MMCIcon *IconList::icon(const QString &key) const { int iconIdx = getIconIndex(key); if (iconIdx == -1) @@ -279,7 +279,7 @@ const MMCIcon *IconList::icon(QString key) return &icons[iconIdx]; } -bool IconList::deleteIcon(QString key) +bool IconList::deleteIcon(const QString &key) { int iconIdx = getIconIndex(key); if (iconIdx == -1) diff --git a/api/gui/icons/IconList.h b/api/gui/icons/IconList.h index 7aeb7067..24ff4454 100644 --- a/api/gui/icons/IconList.h +++ b/api/gui/icons/IconList.h @@ -44,18 +44,19 @@ public: virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; - virtual bool addIcon(QString key, QString name, QString path, IconType type) override; - bool deleteIcon(QString key); - bool iconFileExists(QString key); + bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type) override; + void saveIcon(const QString &key, const QString &path, const char * format) const override; + bool deleteIcon(const QString &key) override; + bool iconFileExists(const QString &key) const override; virtual QStringList mimeTypes() const override; virtual Qt::DropActions supportedDropActions() const override; virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - void installIcons(QStringList iconFiles); + void installIcons(const QStringList &iconFiles) override; - const MMCIcon * icon(QString key); + const MMCIcon * icon(const QString &key) const; void startWatching(); void stopWatching(); diff --git a/api/logic/BaseInstance.cpp b/api/logic/BaseInstance.cpp index 9dee2c38..452e657a 100644 --- a/api/logic/BaseInstance.cpp +++ b/api/logic/BaseInstance.cpp @@ -17,6 +17,7 @@ #include <QFileInfo> #include <QDir> +#include <QDebug> #include "settings/INISettingsObject.h" #include "settings/Setting.h" @@ -74,10 +75,32 @@ void BaseInstance::iconUpdated(QString key) } } +void BaseInstance::invalidate() +{ + changeStatus(Status::Gone); + qDebug() << "Instance" << id() << "has been invalidated."; +} + void BaseInstance::nuke() { + changeStatus(Status::Gone); + qDebug() << "Instance" << id() << "has been deleted by MultiMC."; FS::deletePath(instanceRoot()); - emit nuked(this); +} + +void BaseInstance::changeStatus(BaseInstance::Status newStatus) +{ + Status status = currentStatus(); + if(status != newStatus) + { + m_status = newStatus; + emit statusChanged(status, newStatus); + } +} + +BaseInstance::Status BaseInstance::currentStatus() const +{ + return m_status; } QString BaseInstance::id() const @@ -278,3 +301,19 @@ std::shared_ptr<LaunchTask> BaseInstance::getLaunchTask() { return m_launchProcess; } + +void BaseInstance::setProvider(BaseInstanceProvider* provider) +{ + // only once. + assert(!m_provider); + if(m_provider) + { + qWarning() << "Provider set more than once for instance" << id(); + } + m_provider = provider; +} + +BaseInstanceProvider* BaseInstance::provider() const +{ + return m_provider; +} diff --git a/api/logic/BaseInstance.h b/api/logic/BaseInstance.h index ebaaeb83..56d3d6cb 100644 --- a/api/logic/BaseInstance.h +++ b/api/logic/BaseInstance.h @@ -14,6 +14,7 @@ */ #pragma once +#include <cassert> #include <QObject> #include "QObjectPtr.h" @@ -35,6 +36,7 @@ class QDir; class Task; class LaunchTask; class BaseInstance; +class BaseInstanceProvider; // pointer for lazy people typedef std::shared_ptr<BaseInstance> InstancePtr; @@ -54,6 +56,13 @@ protected: /// no-touchy! BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); +public: /* types */ + enum class Status + { + Present, + Gone // either nuked or invalidated + }; + public: /// virtual destructor to make sure the destruction is COMPLETE virtual ~BaseInstance() {}; @@ -66,6 +75,14 @@ public: /// responsible of cleaning up the husk void nuke(); + /*** + * the instance has been invalidated - it is no longer tracked by MultiMC for some reason, + * but it has not necessarily been deleted. + * + * Happens when the instance folder changes to some other location, or the instance is removed by external means. + */ + void invalidate(); + /// The instance's ID. The ID SHALL be determined by MMC internally. The ID IS guaranteed to /// be unique. virtual QString id() const; @@ -75,6 +92,9 @@ public: int64_t totalTimePlayed() const; void resetTimePlayed(); + void setProvider(BaseInstanceProvider * provider); + BaseInstanceProvider * provider() const; + /// get the type of this instance QString instanceType() const; @@ -219,6 +239,11 @@ public: */ virtual QStringList verboseDescription(AuthSessionPtr session) = 0; + Status currentStatus() const; + +protected: + void changeStatus(Status newStatus); + signals: /*! * \brief Signal emitted when properties relevant to the instance view change @@ -228,10 +253,6 @@ signals: * \brief Signal emitted when groups are affected in any way */ void groupChanged(); - /*! - * \brief The instance just got nuked. Hurray! - */ - void nuked(BaseInstance *inst); void flagsChanged(); @@ -239,10 +260,12 @@ signals: void runningStatusChanged(bool running); + void statusChanged(Status from, Status to); + protected slots: void iconUpdated(QString key); -protected: +protected: /* data */ QString m_rootDir; QString m_group; SettingsObjectPtr m_settings; @@ -250,6 +273,10 @@ protected: bool m_isRunning = false; std::shared_ptr<LaunchTask> m_launchProcess; QDateTime m_timeStarted; + BaseInstanceProvider * m_provider = nullptr; + +private: /* data */ + Status m_status = Status::Present; }; Q_DECLARE_METATYPE(std::shared_ptr<BaseInstance>) diff --git a/api/logic/BaseInstanceProvider.h b/api/logic/BaseInstanceProvider.h new file mode 100644 index 00000000..f6833650 --- /dev/null +++ b/api/logic/BaseInstanceProvider.h @@ -0,0 +1,57 @@ +#pragma once + +#include <QObject> +#include <QString> +#include "BaseInstance.h" +#include "settings/SettingsObject.h" + +#include "multimc_logic_export.h" + +using InstanceId = QString; +using InstanceLocator = std::pair<InstancePtr, int>; + +enum class InstCreateError +{ + NoCreateError = 0, + NoSuchVersion, + UnknownCreateError, + InstExists, + CantCreateDir +}; + +class MULTIMC_LOGIC_EXPORT BaseInstanceProvider : public QObject +{ + Q_OBJECT +public: + BaseInstanceProvider(SettingsObjectPtr settings) : m_globalSettings(settings) + { + // nil + } +public: + virtual QList<InstanceId> discoverInstances() = 0; + virtual InstancePtr loadInstance(const InstanceId &id) = 0; + virtual void loadGroupList() = 0; + virtual void saveGroupList() = 0; + + virtual QString getStagedInstancePath() + { + return QString(); + } + virtual bool commitStagedInstance(const QString & keyPath, const QString & path, const QString& instanceName, const QString & groupName) + { + return false; + } + virtual bool destroyStagingPath(const QString & path) + { + return true; + } + +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); + +protected: + SettingsObjectPtr m_globalSettings; +}; diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index c430b53f..0741fb1a 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -8,8 +8,17 @@ set(CORE_SOURCES BaseInstaller.cpp BaseVersionList.h BaseVersionList.cpp + InstanceCreationTask.h + InstanceCreationTask.cpp + InstanceCopyTask.h + InstanceCopyTask.cpp + InstanceImportTask.h + InstanceImportTask.cpp InstanceList.h InstanceList.cpp + BaseInstanceProvider.h + FolderInstanceProvider.h + FolderInstanceProvider.cpp BaseVersion.h BaseInstance.h BaseInstance.cpp @@ -276,6 +285,8 @@ set(MINECRAFT_SOURCES minecraft/ftb/LegacyFTBInstance.cpp minecraft/ftb/FTBProfileStrategy.h minecraft/ftb/FTBProfileStrategy.cpp + minecraft/ftb/FTBInstanceProvider.cpp + minecraft/ftb/FTBInstanceProvider.h minecraft/ftb/FTBPlugin.h minecraft/ftb/FTBPlugin.cpp diff --git a/api/logic/FolderInstanceProvider.cpp b/api/logic/FolderInstanceProvider.cpp new file mode 100644 index 00000000..a1f3f1f2 --- /dev/null +++ b/api/logic/FolderInstanceProvider.cpp @@ -0,0 +1,356 @@ +#include "FolderInstanceProvider.h" +#include "settings/INISettingsObject.h" +#include "FileSystem.h" +#include "minecraft/onesix/OneSixInstance.h" +#include "minecraft/legacy/LegacyInstance.h" +#include "NullInstance.h" + +#include <QDir> +#include <QDirIterator> +#include <QFileSystemWatcher> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QUuid> + +const static int GROUP_FILE_FORMAT_VERSION = 1; + +struct WatchLock +{ + WatchLock(QFileSystemWatcher * watcher, const QString& instDir) + : m_watcher(watcher), m_instDir(instDir) + { + m_watcher->removePath(m_instDir); + } + ~WatchLock() + { + m_watcher->addPath(m_instDir); + } + QFileSystemWatcher * m_watcher; + QString m_instDir; +}; + +FolderInstanceProvider::FolderInstanceProvider(SettingsObjectPtr settings, const QString& instDir) + : BaseInstanceProvider(settings) +{ + m_instDir = instDir; + if (!QDir::current().exists(m_instDir)) + { + QDir::current().mkpath(m_instDir); + } + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &FolderInstanceProvider::instanceDirContentsChanged); + m_watcher->addPath(m_instDir); +} + +QList< InstanceId > FolderInstanceProvider::discoverInstances() +{ + QList<InstanceId> out; + QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable, 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; + } + 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 OneSixInstance(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(); + inst->setProvider(this); + 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; +} + +#include "InstanceImportTask.h" +Task * FolderInstanceProvider::zipImportTask(const QUrl sourceUrl, const QString& instName, const QString& instGroup, const QString& instIcon) +{ + return new InstanceImportTask(m_globalSettings, sourceUrl, this, instName, instGroup, instIcon); +} + +#include "InstanceCreationTask.h" +Task * FolderInstanceProvider::creationTask(BaseVersionPtr version, const QString& instName, const QString& instGroup, const QString& instIcon) +{ + return new InstanceCreationTask(m_globalSettings, this, version, instName, instIcon, instGroup); +} + +#include "InstanceCopyTask.h" +Task * FolderInstanceProvider::copyTask(const InstancePtr& oldInstance, const QString& instName, const QString& instGroup, const QString& instIcon, bool copySaves) +{ + return new InstanceCopyTask(m_globalSettings, this, oldInstance, instName, instIcon, instGroup, copySaves); +} + +void FolderInstanceProvider::saveGroupList() +{ + 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 (!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()); + } + catch(FS::FileSystemException & e) + { + qCritical() << "Failed to write instance group file :" << e.cause(); + } +} + +void FolderInstanceProvider::loadGroupList() +{ + 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 (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); +} + +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 = value.toString(); + if(newInstDir != m_instDir) + { + if(m_groupsLoaded) + { + saveGroupList(); + } + m_instDir = newInstDir; + m_groupsLoaded = false; + emit instancesChanged(); + } +} + +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& keyPath, const QString& path, const QString& instanceName, + const QString& groupName) +{ + if(!path.contains(keyPath)) + { + qWarning() << "It is not possible to commit" << path << "because it is not in" << keyPath; + return false; + } + QDir dir; + QString instID = FS::DirNameFromString(instanceName, m_instDir); + { + WatchLock lock(m_watcher, m_instDir); + if(!dir.rename(path, FS::PathCombine(m_instDir, instID))) + { + destroyStagingPath(keyPath); + return false; + } + groupMap[instID] = groupName; + emit groupsChanged({groupName}); + emit instancesChanged(); + } + saveGroupList(); + return destroyStagingPath(keyPath); +} + +bool FolderInstanceProvider::destroyStagingPath(const QString& keyPath) +{ + return FS::deletePath(keyPath); +} + diff --git a/api/logic/FolderInstanceProvider.h b/api/logic/FolderInstanceProvider.h new file mode 100644 index 00000000..f350a96d --- /dev/null +++ b/api/logic/FolderInstanceProvider.h @@ -0,0 +1,63 @@ +#pragma once + +#include "BaseInstanceProvider.h" +#include <QMap> + +class QFileSystemWatcher; + +class MULTIMC_LOGIC_EXPORT FolderInstanceProvider : public BaseInstanceProvider +{ + Q_OBJECT +public: + FolderInstanceProvider(SettingsObjectPtr settings, const QString & instDir); + +public: + /// used by InstanceList to @return a list of plausible IDs to probe for + QList<InstanceId> discoverInstances() override; + + /// used by InstanceList to (re)load an instance with the given @id. + InstancePtr loadInstance(const InstanceId& id) override; + + + // create instance in this provider + Task * creationTask(BaseVersionPtr version, const QString &instName, const QString &instGroup, const QString &instIcon); + + // copy instance to this provider + Task * copyTask(const InstancePtr &oldInstance, const QString& instName, const QString& instGroup, const QString& instIcon, bool copySaves); + + // import zipped instance into this provider + Task * zipImportTask(const QUrl sourceUrl, const QString &instName, const QString &instGroup, const QString &instIcon); + + /** + * 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() override; + /** + * 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 & path, const QString& instanceName, const QString & groupName) override; + /** + * Destroy a previously created staging area given by @keyPath - used when creation fails. + * Used by instance manipulation tasks. + */ + bool destroyStagingPath(const QString & keyPath) override; + +public slots: + void on_InstFolderChanged(const Setting &setting, QVariant value); + +private slots: + void instanceDirContentsChanged(const QString &path); + void groupChanged(); + +private: /* methods */ + void loadGroupList() override; + void saveGroupList() override; + +private: /* data */ + QString m_instDir; + QFileSystemWatcher * m_watcher; + QMap<QString, QString> groupMap; + bool m_groupsLoaded = false; +}; diff --git a/api/logic/InstanceCopyTask.cpp b/api/logic/InstanceCopyTask.cpp new file mode 100644 index 00000000..1e231478 --- /dev/null +++ b/api/logic/InstanceCopyTask.cpp @@ -0,0 +1,53 @@ +#include "InstanceCopyTask.h" +#include "BaseInstanceProvider.h" +#include "settings/INISettingsObject.h" +#include "FileSystem.h" +#include "NullInstance.h" +#include "pathmatcher/RegexpMatcher.h" + +InstanceCopyTask::InstanceCopyTask(SettingsObjectPtr settings, BaseInstanceProvider* target, InstancePtr origInstance, const QString& instName, const QString& instIcon, const QString& instGroup, bool copySaves) +{ + m_globalSettings = settings; + m_target = target; + m_origInstance = origInstance; + m_instName = instName; + m_instIcon = instIcon; + m_instGroup = instGroup; + m_copySaves = copySaves; +} + +void InstanceCopyTask::executeTask() +{ + setStatus(tr("Copying instance %1").arg(m_origInstance->name())); + std::unique_ptr<IPathMatcher> matcher; + if(!m_copySaves) + { + // FIXME: get this from the original instance type... + auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); + matcherReal->caseSensitive(false); + matcher.reset(matcherReal); + } + + QString stagingPath = m_target->getStagedInstancePath(); + FS::copy folderCopy(m_origInstance->instanceRoot(), stagingPath); + folderCopy.followSymlinks(false).blacklist(matcher.get()); + if (!folderCopy()) + { + m_target->destroyStagingPath(stagingPath); + emitFailed(tr("Instance folder copy failed.")); + return; + } + + // FIXME: shouldn't this be able to report errors? + auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(stagingPath, "instance.cfg")); + instanceSettings->registerSetting("InstanceType", "Legacy"); + + // FIXME: and this too? errors??? + m_origInstance->copy(stagingPath); + + InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, stagingPath)); + inst->setName(m_instName); + inst->setIconKey(m_instIcon); + m_target->commitStagedInstance(stagingPath, stagingPath, m_instName, m_instGroup); + emitSucceeded(); +} diff --git a/api/logic/InstanceCopyTask.h b/api/logic/InstanceCopyTask.h new file mode 100644 index 00000000..a663ddbd --- /dev/null +++ b/api/logic/InstanceCopyTask.h @@ -0,0 +1,34 @@ +#pragma once + +#include "tasks/Task.h" +#include "multimc_logic_export.h" +#include "net/NetJob.h" +#include <QUrl> +#include "settings/SettingsObject.h" +#include "BaseVersion.h" +#include "BaseInstance.h" + +class BaseInstanceProvider; + +class MULTIMC_LOGIC_EXPORT InstanceCopyTask : public Task +{ + Q_OBJECT +public: + explicit InstanceCopyTask(SettingsObjectPtr settings, BaseInstanceProvider * target, InstancePtr origInstance, const QString &instName, + const QString &instIcon, const QString &instGroup, bool copySaves); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private: /* data */ + SettingsObjectPtr m_globalSettings; + BaseInstanceProvider * m_target = nullptr; + InstancePtr m_origInstance; + QString m_instName; + QString m_instIcon; + QString m_instGroup; + bool m_copySaves = false; +}; + + diff --git a/api/logic/InstanceCreationTask.cpp b/api/logic/InstanceCreationTask.cpp new file mode 100644 index 00000000..00156701 --- /dev/null +++ b/api/logic/InstanceCreationTask.cpp @@ -0,0 +1,46 @@ +#include "InstanceCreationTask.h" +#include "BaseInstanceProvider.h" +#include "settings/INISettingsObject.h" +#include "FileSystem.h" + +//FIXME: remove this +#include "minecraft/MinecraftVersion.h" +#include "minecraft/onesix/OneSixInstance.h" + +InstanceCreationTask::InstanceCreationTask(SettingsObjectPtr settings, BaseInstanceProvider* target, BaseVersionPtr version, + const QString& instName, const QString& instIcon, const QString& instGroup) +{ + m_globalSettings = settings; + m_target = target; + m_instName = instName; + m_instIcon = instIcon; + m_instGroup = instGroup; + m_version = version; +} + +void InstanceCreationTask::executeTask() +{ + setStatus(tr("Creating instance from version %1").arg(m_version->name())); + auto minecraftVersion = std::dynamic_pointer_cast<MinecraftVersion>(m_version); + if(!minecraftVersion) + { + emitFailed(tr("The supplied version is not a Minecraft version.")); + return ; + } + + QString stagingPath = m_target->getStagedInstancePath(); + QDir rootDir(stagingPath); + + auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(stagingPath, "instance.cfg")); + instanceSettings->registerSetting("InstanceType", "Legacy"); + + auto mcVer = std::dynamic_pointer_cast<MinecraftVersion>(m_version); + instanceSettings->set("InstanceType", "OneSix"); + InstancePtr inst(new OneSixInstance(m_globalSettings, instanceSettings, stagingPath)); + inst->setIntendedVersionId(m_version->descriptor()); + inst->setName(m_instName); + inst->setIconKey(m_instIcon); + inst->init(); + m_target->commitStagedInstance(stagingPath, stagingPath, m_instName, m_instGroup); + emitSucceeded(); +} diff --git a/api/logic/InstanceCreationTask.h b/api/logic/InstanceCreationTask.h new file mode 100644 index 00000000..b4ade320 --- /dev/null +++ b/api/logic/InstanceCreationTask.h @@ -0,0 +1,31 @@ +#pragma once + +#include "tasks/Task.h" +#include "multimc_logic_export.h" +#include "net/NetJob.h" +#include <QUrl> +#include "settings/SettingsObject.h" +#include "BaseVersion.h" + +class BaseInstanceProvider; + +class MULTIMC_LOGIC_EXPORT InstanceCreationTask : public Task +{ + Q_OBJECT +public: + explicit InstanceCreationTask(SettingsObjectPtr settings, BaseInstanceProvider * target, BaseVersionPtr version, const QString &instName, + const QString &instIcon, const QString &instGroup); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private: /* data */ + SettingsObjectPtr m_globalSettings; + BaseInstanceProvider * m_target; + BaseVersionPtr m_version; + QString m_instName; + QString m_instIcon; + QString m_instGroup; +}; + diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp new file mode 100644 index 00000000..99f2553f --- /dev/null +++ b/api/logic/InstanceImportTask.cpp @@ -0,0 +1,146 @@ + +#include "InstanceImportTask.h" +#include "BaseInstance.h" +#include "BaseInstanceProvider.h" +#include "FileSystem.h" +#include "Env.h" +#include "MMCZip.h" +#include "NullInstance.h" +#include "settings/INISettingsObject.h" +#include "icons/IIconList.h" + +InstanceImportTask::InstanceImportTask(SettingsObjectPtr settings, const QUrl sourceUrl, BaseInstanceProvider * target, + const QString &instName, const QString &instIcon, const QString &instGroup) +{ + m_globalSettings = settings; + m_sourceUrl = sourceUrl; + m_target = target; + m_instName = instName; + m_instIcon = instIcon; + m_instGroup = instGroup; +} + +void InstanceImportTask::executeTask() +{ + InstancePtr newInstance; + + if (m_sourceUrl.isLocalFile()) + { + m_archivePath = m_sourceUrl.toLocalFile(); + extractAndTweak(); + } + else + { + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + m_downloadRequired = true; + + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = ENV.metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob.reset(new NetJob(tr("Modpack download"))); + m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); + connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); + connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed); + m_filesNetJob->start(); + } +} + +void InstanceImportTask::downloadSucceeded() +{ + extractAndTweak(); +} + +void InstanceImportTask::downloadFailed(QString reason) +{ + emitFailed(reason); +} + +void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) +{ + setProgress(current / 2, total); +} + +static QFileInfo findRecursive(const QString &dir, const QString &name) +{ + for (const auto info : QDir(dir).entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files, QDir::DirsLast)) + { + if (info.isFile() && info.fileName() == name) + { + return info; + } + else if (info.isDir()) + { + const QFileInfo res = findRecursive(info.absoluteFilePath(), name); + if (res.isFile() && res.exists()) + { + return res; + } + } + } + return QFileInfo(); +} + +void InstanceImportTask::extractAndTweak() +{ + setStatus(tr("Extracting modpack")); + QString stagingPath = m_target->getStagedInstancePath(); + QDir extractDir(stagingPath); + qDebug() << "Attempting to create instance from" << m_archivePath; + if (MMCZip::extractDir(m_archivePath, extractDir.absolutePath()).isEmpty()) + { + m_target->destroyStagingPath(stagingPath); + emitFailed(tr("Failed to extract modpack")); + return; + } + const QFileInfo instanceCfgFile = findRecursive(extractDir.absolutePath(), "instance.cfg"); + if (!instanceCfgFile.isFile() || !instanceCfgFile.exists()) + { + m_target->destroyStagingPath(stagingPath); + emitFailed(tr("Archive does not contain instance.cfg")); + return; + } + + // FIXME: copy from FolderInstanceProvider!!! FIX IT!!! + auto instanceSettings = std::make_shared<INISettingsObject>(instanceCfgFile.absoluteFilePath()); + instanceSettings->registerSetting("InstanceType", "Legacy"); + + QString actualDir = instanceCfgFile.absolutePath(); + NullInstance instance(m_globalSettings, instanceSettings, actualDir); + + // reset time played on import... because packs. + instance.resetTimePlayed(); + + // set a new nice name + instance.setName(m_instName); + + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + else + { + m_instIcon = instance.iconKey(); + auto importIconPath = FS::PathCombine(instance.instanceRoot(), m_instIcon + ".png"); + if (QFile::exists(importIconPath)) + { + // import icon + auto iconList = ENV.icons(); + if (iconList->iconFileExists(m_instIcon)) + { + iconList->deleteIcon(m_instIcon); + } + iconList->installIcons({importIconPath}); + } + } + if (!m_target->commitStagedInstance(stagingPath, actualDir, m_instName, m_instGroup)) + { + m_target->destroyStagingPath(stagingPath); + emitFailed(tr("Unable to commit instance")); + return; + } + emitSucceeded(); +} diff --git a/api/logic/InstanceImportTask.h b/api/logic/InstanceImportTask.h new file mode 100644 index 00000000..a551ec68 --- /dev/null +++ b/api/logic/InstanceImportTask.h @@ -0,0 +1,40 @@ +#pragma once + +#include "tasks/Task.h" +#include "multimc_logic_export.h" +#include "net/NetJob.h" +#include <QUrl> +#include "settings/SettingsObject.h" + +class BaseInstanceProvider; + +class MULTIMC_LOGIC_EXPORT InstanceImportTask : public Task +{ + Q_OBJECT +public: + explicit InstanceImportTask(SettingsObjectPtr settings, const QUrl sourceUrl, BaseInstanceProvider * target, const QString &instName, + const QString &instIcon, const QString &instGroup); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private: + void extractAndTweak(); + +private slots: + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + +private: /* data */ + SettingsObjectPtr m_globalSettings; + NetJobPtr m_filesNetJob; + QUrl m_sourceUrl; + BaseInstanceProvider * m_target; + QString m_archivePath; + bool m_downloadRequired = false; + QString m_instName; + QString m_instIcon; + QString m_instGroup; +}; diff --git a/api/logic/InstanceList.cpp b/api/logic/InstanceList.cpp index d89b8ea7..9bdcc113 100644 --- a/api/logic/InstanceList.cpp +++ b/api/logic/InstanceList.cpp @@ -16,39 +16,21 @@ #include <QDir> #include <QSet> #include <QFile> -#include <QDirIterator> #include <QThread> #include <QTextStream> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> #include <QXmlStreamReader> -#include <QRegularExpression> #include <QDebug> #include "InstanceList.h" #include "BaseInstance.h" -//FIXME: this really doesn't belong *here* -#include "minecraft/onesix/OneSixInstance.h" -#include "minecraft/legacy/LegacyInstance.h" -#include "minecraft/ftb/FTBPlugin.h" -#include "minecraft/MinecraftVersion.h" -#include "settings/INISettingsObject.h" -#include "NullInstance.h" -#include "FileSystem.h" -#include "pathmatcher/RegexpMatcher.h" - -const static int GROUP_FILE_FORMAT_VERSION = 1; +#include "FolderInstanceProvider.h" InstanceList::InstanceList(SettingsObjectPtr globalSettings, const QString &instDir, QObject *parent) : QAbstractListModel(parent), m_instDir(instDir) { m_globalSettings = globalSettings; - if (!QDir::current().exists(m_instDir)) - { - QDir::current().mkpath(m_instDir); - } + resumeWatch(); } InstanceList::~InstanceList() @@ -120,34 +102,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const return f; } -void InstanceList::groupChanged() -{ - // save the groups. save all of them. - saveGroupList(); -} - QStringList InstanceList::getGroups() { return m_groups.toList(); } -void InstanceList::suspendGroupSaving() -{ - suspendedGroupSave = true; -} - -void InstanceList::resumeGroupSaving() -{ - if(suspendedGroupSave) - { - suspendedGroupSave = false; - if(queuedGroupSave) - { - saveGroupList(); - } - } -} - void InstanceList::deleteGroup(const QString& name) { for(auto & instance: m_instances) @@ -160,230 +119,180 @@ void InstanceList::deleteGroup(const QString& name) } } -void InstanceList::saveGroupList() +static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list) { - if(suspendedGroupSave) + QMap<InstanceId, InstanceLocator> out; + int i = 0; + for(auto & item: list) { - queuedGroupSave = true; - return; + auto id = item->id(); + if(out.contains(id)) + { + qWarning() << "Duplicate ID" << id << "in instance list"; + } + out[id] = std::make_pair(item, i); + i++; } + return out; +} - QString groupFileName = m_instDir + "/instgroups.json"; - QMap<QString, QSet<QString>> groupMap; - for (auto instance : m_instances) - { - QString id = instance->id(); - QString group = instance->group(); - if (group.isEmpty()) - continue; +InstanceList::InstListError InstanceList::loadList(bool complete) +{ + auto existingIds = getIdMapping(m_instances); - // keep a list/set of groups for choosing - m_groups.insert(group); + QList<InstancePtr> newList; - if (!groupMap.count(group)) + auto processIds = [&](BaseInstanceProvider * provider, QList<InstanceId> ids) + { + for(auto & id: ids) { - QSet<QString> set; - set.insert(id); - groupMap[group] = set; + if(existingIds.contains(id)) + { + auto instPair = existingIds[id]; + /* + auto & instPtr = instPair.first; + auto & instIdx = instPair.second; + */ + existingIds.remove(id); + qDebug() << "Should keep and soft-reload" << id; + } + else + { + InstancePtr instPtr = provider->loadInstance(id); + newList.append(instPtr); + } } - else + }; + if(complete) + { + for(auto & item: m_providers) { - QSet<QString> &set = groupMap[group]; - set.insert(id); + processIds(item.get(), item->discoverInstances()); } } - QJsonObject toplevel; - toplevel.insert("formatVersion", QJsonValue(QString("1"))); - QJsonObject groupsArr; - for (auto iter = groupMap.begin(); iter != groupMap.end(); iter++) + else { - auto list = iter.value(); - auto name = iter.key(); - QJsonObject groupObj; - QJsonArray instanceArr; - groupObj.insert("hidden", QJsonValue(QString("false"))); - for (auto item : list) + for (auto & item: m_updatedProviders) { - instanceArr.append(QJsonValue(item)); + processIds(item, item->discoverInstances()); } - groupObj.insert("instances", instanceArr); - groupsArr.insert(name, groupObj); } - toplevel.insert("groups", groupsArr); - QJsonDocument doc(toplevel); - try + + // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. + if(!existingIds.isEmpty()) { - FS::write(groupFileName, doc.toJson()); + // 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; + }; + 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 = [&]() + { + 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) + { + auto instPtr = removedItem.first; + if(!complete && !m_updatedProviders.contains(instPtr->provider())) + { + continue; + } + instPtr->invalidate(); + currentItem = removedItem.second; + if(back_bookmark == -1) + { + // no bookmark yet + back_bookmark = currentItem; + } + else if(currentItem == front_bookmark - 1) + { + // part of contiguous sequence, continue + } + else + { + // seam between previous and current item + removeNow(); + } + front_bookmark = currentItem; + } + if(back_bookmark != -1) + { + removeNow(); + } } - catch(FS::FileSystemException & e) + if(newList.size()) { - qCritical() << "Failed to write instance group file :" << e.cause(); + add(newList); } + m_updatedProviders.clear(); + return NoError; } -void InstanceList::loadGroupList(QMap<QString, QString> &groupMap) +void InstanceList::add(const QList<InstancePtr> &t) { - 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 (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) + beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); + m_instances.append(t); + for(auto & ptr : t) { - qCritical() << QString("Failed to parse instance group file: %1 at offset %2") - .arg(error.errorString(), QString::number(error.offset)) - .toUtf8(); - return; + connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } + endInsertRows(); +} - // if the root of the json wasn't an object, fail - if (!jsonDoc.isObject()) +void InstanceList::resumeWatch() +{ + if(m_watchLevel > 0) { - qWarning() << "Invalid group file. Root entry should be an object."; + qWarning() << "Bad suspend level resume in instance list"; 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()) + m_watchLevel++; + if(m_watchLevel > 0 && !m_updatedProviders.isEmpty()) { - qWarning() << "Invalid group list JSON: 'groups' should be an object."; - return; - } - - // 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 - m_groups.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; - } + loadList(); } } -InstanceList::InstListError InstanceList::loadList() +void InstanceList::suspendWatch() { - // load the instance groups - QMap<QString, QString> groupMap; - loadGroupList(groupMap); + m_watchLevel --; +} - QList<InstancePtr> tempList; +void InstanceList::providerUpdated() +{ + auto provider = dynamic_cast<BaseInstanceProvider *>(QObject::sender()); + if(!provider) { - QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable, - QDirIterator::FollowSymlinks); - while (iter.hasNext()) - { - QString subDir = iter.next(); - if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) - continue; - qDebug() << "Loading MultiMC instance from " << subDir; - InstancePtr instPtr; - auto error = loadInstance(instPtr, subDir); - if(!continueProcessInstance(instPtr, error, subDir, groupMap)) - continue; - tempList.append(instPtr); - } + qWarning() << "InstanceList::providerUpdated triggered by a non-provider"; + return; } - - // FIXME: generalize - FTBPlugin::loadInstances(m_globalSettings, groupMap, tempList); - - beginResetModel(); - m_instances.clear(); - for(auto inst: tempList) + m_updatedProviders.insert(provider); + if(m_watchLevel == 1) { - connect(inst.get(), SIGNAL(propertiesChanged(BaseInstance *)), this, - SLOT(propertiesChanged(BaseInstance *))); - connect(inst.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged())); - connect(inst.get(), SIGNAL(nuked(BaseInstance *)), this, - SLOT(instanceNuked(BaseInstance *))); - m_instances.append(inst); + loadList(); } - endResetModel(); - emit dataIsInvalid(); - return NoError; -} - -/// Clear all instances. Triggers notifications. -void InstanceList::clear() -{ - beginResetModel(); - saveGroupList(); - m_instances.clear(); - endResetModel(); - emit dataIsInvalid(); } -void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) +void InstanceList::groupsPublished(QSet<QString> newGroups) { - m_instDir = value.toString(); - loadList(); + m_groups.unite(newGroups); } -/// Add an instance. Triggers notifications, returns the new index -int InstanceList::add(InstancePtr t) +void InstanceList::addInstanceProvider(BaseInstanceProvider* provider) { - beginInsertRows(QModelIndex(), m_instances.size(), m_instances.size()); - m_instances.append(t); - t->setParent(this); - connect(t.get(), SIGNAL(propertiesChanged(BaseInstance *)), this, - SLOT(propertiesChanged(BaseInstance *))); - connect(t.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged())); - connect(t.get(), SIGNAL(nuked(BaseInstance *)), this, SLOT(instanceNuked(BaseInstance *))); - endInsertRows(); - return count() - 1; + connect(provider, &BaseInstanceProvider::instancesChanged, this, &InstanceList::providerUpdated); + connect(provider, &BaseInstanceProvider::groupsChanged, this, &InstanceList::groupsPublished); + m_providers.append(provider); } InstancePtr InstanceList::getInstanceById(QString instId) const @@ -418,157 +327,6 @@ int InstanceList::getInstIndex(BaseInstance *inst) const return -1; } -bool InstanceList::continueProcessInstance(InstancePtr instPtr, const int error, - const QDir &dir, QMap<QString, QString> &groupMap) -{ - if (error != InstanceList::NoLoadError && error != InstanceList::NotAnInstance) - { - QString errorMsg = QString("Failed to load instance %1: ") - .arg(QFileInfo(dir.absolutePath()).baseName()) - .toUtf8(); - - switch (error) - { - default: - errorMsg += QString("Unknown instance loader error %1").arg(error); - break; - } - qCritical() << errorMsg.toUtf8(); - return false; - } - else if (!instPtr) - { - qCritical() << QString("Error loading instance %1. Instance loader returned null.") - .arg(QFileInfo(dir.absolutePath()).baseName()) - .toUtf8(); - return false; - } - else - { - auto iter = groupMap.find(instPtr->id()); - if (iter != groupMap.end()) - { - instPtr->setGroupInitial((*iter)); - } - qDebug() << "Loaded instance " << instPtr->name() << " from " << dir.absolutePath(); - return true; - } -} - -InstanceList::InstLoadError -InstanceList::loadInstance(InstancePtr &inst, const QString &instDir) -{ - auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instDir, "instance.cfg")); - - instanceSettings->registerSetting("InstanceType", "Legacy"); - - QString inst_type = instanceSettings->get("InstanceType").toString(); - - // FIXME: replace with a map lookup, where instance classes register their types - if (inst_type == "OneSix" || inst_type == "Nostalgia") - { - inst.reset(new OneSixInstance(m_globalSettings, instanceSettings, instDir)); - } - else if (inst_type == "Legacy") - { - inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instDir)); - } - else - { - inst.reset(new NullInstance(m_globalSettings, instanceSettings, instDir)); - } - inst->init(); - return NoLoadError; -} - -InstanceList::InstCreateError -InstanceList::createInstance(InstancePtr &inst, BaseVersionPtr version, const QString &instDir) -{ - QDir rootDir(instDir); - - qDebug() << instDir.toUtf8(); - if (!rootDir.exists() && !rootDir.mkpath(".")) - { - qCritical() << "Can't create instance folder" << instDir; - return InstanceList::CantCreateDir; - } - - if (!version) - { - qCritical() << "Can't create instance for non-existing MC version"; - return InstanceList::NoSuchVersion; - } - - auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instDir, "instance.cfg")); - instanceSettings->registerSetting("InstanceType", "Legacy"); - - auto minecraftVersion = std::dynamic_pointer_cast<MinecraftVersion>(version); - if(minecraftVersion) - { - auto mcVer = std::dynamic_pointer_cast<MinecraftVersion>(version); - instanceSettings->set("InstanceType", "OneSix"); - inst.reset(new OneSixInstance(m_globalSettings, instanceSettings, instDir)); - inst->setIntendedVersionId(version->descriptor()); - inst->init(); - return InstanceList::NoCreateError; - } - return InstanceList::NoSuchVersion; -} - -InstanceList::InstCreateError -InstanceList::copyInstance(InstancePtr &newInstance, InstancePtr &oldInstance, const QString &instDir, bool copySaves) -{ - QDir rootDir(instDir); - std::unique_ptr<IPathMatcher> matcher; - if(!copySaves) - { - auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); - matcherReal->caseSensitive(false); - matcher.reset(matcherReal); - } - - qDebug() << instDir.toUtf8(); - FS::copy folderCopy(oldInstance->instanceRoot(), instDir); - folderCopy.followSymlinks(false).blacklist(matcher.get()); - if (!folderCopy()) - { - FS::deletePath(instDir); - return InstanceList::CantCreateDir; - } - - INISettingsObject settings_obj(FS::PathCombine(instDir, "instance.cfg")); - settings_obj.registerSetting("InstanceType", "Legacy"); - QString inst_type = settings_obj.get("InstanceType").toString(); - - oldInstance->copy(instDir); - - auto error = loadInstance(newInstance, instDir); - - switch (error) - { - case NoLoadError: - return NoCreateError; - case NotAnInstance: - rootDir.removeRecursively(); - return CantCreateDir; - default: - case UnknownLoadError: - rootDir.removeRecursively(); - return UnknownCreateError; - } -} - -void InstanceList::instanceNuked(BaseInstance *inst) -{ - int i = getInstIndex(inst); - if (i != -1) - { - beginRemoveRows(QModelIndex(), i, i); - m_instances.removeAt(i); - endRemoveRows(); - } -} - void InstanceList::propertiesChanged(BaseInstance *inst) { int i = getInstIndex(inst); @@ -577,3 +335,5 @@ void InstanceList::propertiesChanged(BaseInstance *inst) emit dataChanged(index(i), index(i)); } } + +#include "InstanceList.moc" diff --git a/api/logic/InstanceList.h b/api/logic/InstanceList.h index 074cca7c..b643de85 100644 --- a/api/logic/InstanceList.h +++ b/api/logic/InstanceList.h @@ -18,24 +18,22 @@ #include <QObject> #include <QAbstractListModel> #include <QSet> +#include <QList> #include "BaseInstance.h" +#include "BaseInstanceProvider.h" #include "multimc_logic_export.h" +#include "QObjectPtr.h" + +class QFileSystemWatcher; class BaseInstance; class QDir; class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel { Q_OBJECT -private: - void loadGroupList(QMap<QString, QString> &groupList); - void suspendGroupSaving(); - void resumeGroupSaving(); - -public slots: - void saveGroupList(); public: explicit InstanceList(SettingsObjectPtr globalSettings, const QString &instDir, QObject *parent = 0); @@ -64,124 +62,47 @@ public: UnknownError }; - enum InstLoadError - { - NoLoadError = 0, - UnknownLoadError, - NotAnInstance - }; - - enum InstCreateError - { - NoCreateError = 0, - NoSuchVersion, - UnknownCreateError, - InstExists, - CantCreateDir - }; - - QString instDir() const - { - return m_instDir; - } - - /*! - * \brief Get the instance at index - */ InstancePtr at(int i) const { return m_instances.at(i); } - ; - /*! - * \brief Get the count of loaded instances - */ int count() const { return m_instances.count(); } - ; - /// Clear all instances. Triggers notifications. - void clear(); + InstListError loadList(bool complete = false); - /// Add an instance. Triggers notifications, returns the new index - int add(InstancePtr t); + /// Add an instance provider. Takes ownership of it. Should only be done before the first load. + void addInstanceProvider(BaseInstanceProvider * provider); - /// Get an instance by ID InstancePtr getInstanceById(QString id) const; - QModelIndex getInstanceIndexById(const QString &id) const; - - // FIXME: instead of iterating through all instances and forming a set, keep the set around QStringList getGroups(); void deleteGroup(const QString & name); - /*! - * \brief Creates a stub instance - * - * \param inst Pointer to store the created instance in. - * \param version Game version to use for the instance - * \param instDir The new instance's directory. - * \return An InstCreateError error code. - * - InstExists if the given instance directory is already an instance. - * - CantCreateDir if the given instance directory cannot be created. - */ - InstCreateError createInstance(InstancePtr &inst, BaseVersionPtr version, - const QString &instDir); - - /*! - * \brief Creates a copy of an existing instance with a new name - * - * \param newInstance Pointer to store the created instance in. - * \param oldInstance The instance to copy - * \param instDir The new instance's directory. - * \return An InstCreateError error code. - * - InstExists if the given instance directory is already an instance. - * - CantCreateDir if the given instance directory cannot be created. - */ - InstCreateError copyInstance(InstancePtr &newInstance, InstancePtr &oldInstance, - const QString &instDir, bool copySaves); - - /*! - * \brief Loads an instance from the given directory. - * Checks the instance's INI file to figure out what the instance's type is first. - * \param inst Pointer to store the loaded instance in. - * \param instDir The instance's directory. - * \return An InstLoadError error code. - * - NotAnInstance if the given instance directory isn't a valid instance. - */ - InstLoadError loadInstance(InstancePtr &inst, const QString &instDir); - signals: void dataIsInvalid(); -public slots: - void on_InstFolderChanged(const Setting &setting, QVariant value); - - /*! - * \brief Loads the instance list. Triggers notifications. - */ - InstListError loadList(); - private slots: void propertiesChanged(BaseInstance *inst); - void instanceNuked(BaseInstance *inst); - void groupChanged(); + void groupsPublished(QSet<QString>); + void providerUpdated(); private: int getInstIndex(BaseInstance *inst) const; - -public: - static bool continueProcessInstance(InstancePtr instPtr, const int error, const QDir &dir, QMap<QString, QString> &groupMap); + void suspendWatch(); + void resumeWatch(); + void add(const QList<InstancePtr> &list); protected: + int m_watchLevel = 0; + QSet<BaseInstanceProvider *> m_updatedProviders; QString m_instDir; QList<InstancePtr> m_instances; QSet<QString> m_groups; SettingsObjectPtr m_globalSettings; - bool suspendedGroupSave = false; - bool queuedGroupSave = false; + QVector<shared_qobject_ptr<BaseInstanceProvider>> m_providers; }; diff --git a/api/logic/icons/IIconList.h b/api/logic/icons/IIconList.h index ec94cb35..ebf99d05 100644 --- a/api/logic/icons/IIconList.h +++ b/api/logic/icons/IIconList.h @@ -1,6 +1,7 @@ #pragma once #include <QString> +#include <QStringList> #include "multimc_logic_export.h" enum IconType : unsigned @@ -16,6 +17,9 @@ class MULTIMC_LOGIC_EXPORT IIconList { public: virtual ~IIconList(); - virtual bool addIcon(QString key, QString name, QString path, IconType type) = 0; + virtual bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type) = 0; + virtual bool deleteIcon(const QString &key) = 0; + virtual void saveIcon(const QString &key, const QString &path, const char * format) const = 0; + virtual bool iconFileExists(const QString &key) const = 0; + virtual void installIcons(const QStringList &iconFiles) = 0; }; - diff --git a/api/logic/minecraft/ftb/FTBInstanceProvider.cpp b/api/logic/minecraft/ftb/FTBInstanceProvider.cpp new file mode 100644 index 00000000..a583c039 --- /dev/null +++ b/api/logic/minecraft/ftb/FTBInstanceProvider.cpp @@ -0,0 +1,278 @@ +#include "FTBInstanceProvider.h" + +#include <QDir> +#include <QDebug> +#include <QXmlStreamReader> +#include <QRegularExpression> + +#include <settings/INISettingsObject.h> +#include <FileSystem.h> + +#include "Env.h" +#include "minecraft/MinecraftVersion.h" + +#include "LegacyFTBInstance.h" +#include "OneSixFTBInstance.h" + +inline uint qHash(FTBRecord record) +{ + return qHash(record.instanceDir); +} + +FTBInstanceProvider::FTBInstanceProvider(SettingsObjectPtr settings) + : BaseInstanceProvider(settings) +{ + // nil +} + +QList<InstanceId> FTBInstanceProvider::discoverInstances() +{ + // nothing to load when we don't have + if (m_globalSettings->get("TrackFTBInstances").toBool() != true) + { + return {}; + } + m_records.clear(); + discoverFTBEntries(); + return m_records.keys(); +} + +InstancePtr FTBInstanceProvider::loadInstance(const InstanceId& id) +{ + // process the records we acquired. + auto iter = m_records.find(id); + if(iter == m_records.end()) + { + qWarning() << "Cannot load instance" << id << "without a record"; + return nullptr; + } + auto & record = m_records[id]; + qDebug() << "Loading FTB instance from " << record.instanceDir; + QString iconKey = record.iconKey; + auto icons = ENV.icons(); + if(icons) + { + icons->addIcon(iconKey, iconKey, FS::PathCombine(record.templateDir, record.logo), IconType::Transient); + } + auto settingsFilePath = FS::PathCombine(record.instanceDir, "instance.cfg"); + qDebug() << "ICON get!"; + + if (QFileInfo(settingsFilePath).exists()) + { + auto instPtr = loadInstance(record); + if (!instPtr) + { + qWarning() << "Couldn't load instance config:" << settingsFilePath; + if(!QFile::remove(settingsFilePath)) + { + qWarning() << "Couldn't remove broken instance config!"; + return nullptr; + } + // failed to load, but removed the poisonous file + } + else + { + return InstancePtr(instPtr); + } + } + auto instPtr = createInstance(record); + if (!instPtr) + { + qWarning() << "Couldn't create FTB instance!"; + return nullptr; + } + return InstancePtr(instPtr); +} + +void FTBInstanceProvider::discoverFTBEntries() +{ + QDir dir = QDir(m_globalSettings->get("FTBLauncherLocal").toString()); + QDir dataDir = QDir(m_globalSettings->get("FTBRoot").toString()); + if (!dataDir.exists()) + { + qDebug() << "The FTB directory specified does not exist. Please check your settings"; + return; + } + else if (!dir.exists()) + { + qDebug() << "The FTB launcher data directory specified does not exist. Please check " + "your settings"; + return; + } + dir.cd("ModPacks"); + auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name); + for (auto filename : allFiles) + { + if (!filename.endsWith(".xml")) + continue; + auto fpath = dir.absoluteFilePath(filename); + QFile f(fpath); + qDebug() << "Discovering FTB instances -- " << fpath; + if (!f.open(QFile::ReadOnly)) + continue; + + // read the FTB packs XML. + QXmlStreamReader reader(&f); + while (!reader.atEnd()) + { + switch (reader.readNext()) + { + case QXmlStreamReader::StartElement: + { + if (reader.name() == "modpack") + { + QXmlStreamAttributes attrs = reader.attributes(); + FTBRecord record; + record.dirName = attrs.value("dir").toString(); + record.instanceDir = dataDir.absoluteFilePath(record.dirName); + record.templateDir = dir.absoluteFilePath(record.dirName); + QDir test(record.instanceDir); + qDebug() << dataDir.absolutePath() << record.instanceDir << record.dirName; + if (!test.exists()) + continue; + record.name = attrs.value("name").toString(); + record.logo = attrs.value("logo").toString(); + QString logo = record.logo; + record.iconKey = logo.remove(QRegularExpression("\\..*")); + auto customVersions = attrs.value("customMCVersions"); + if (!customVersions.isNull()) + { + QMap<QString, QString> versionMatcher; + QString customVersionsStr = customVersions.toString(); + QStringList list = customVersionsStr.split(';'); + for (auto item : list) + { + auto segment = item.split('^'); + if (segment.size() != 2) + { + qCritical() << "FTB: Segment of size < 2 in " + << customVersionsStr; + continue; + } + versionMatcher[segment[0]] = segment[1]; + } + auto actualVersion = attrs.value("version").toString(); + if (versionMatcher.contains(actualVersion)) + { + record.mcVersion = versionMatcher[actualVersion]; + } + else + { + record.mcVersion = attrs.value("mcVersion").toString(); + } + } + else + { + record.mcVersion = attrs.value("mcVersion").toString(); + } + record.description = attrs.value("description").toString(); + auto id = "FTB/" + record.dirName; + m_records[id] = record; + } + break; + } + case QXmlStreamReader::EndElement: + break; + case QXmlStreamReader::Characters: + break; + default: + break; + } + } + f.close(); + } +} + +InstancePtr FTBInstanceProvider::loadInstance(const FTBRecord & record) const +{ + InstancePtr inst; + + auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg")); + m_settings->registerSetting("InstanceType", "Legacy"); + + qDebug() << "Loading existing " << record.name; + + QString inst_type = m_settings->get("InstanceType").toString(); + if (inst_type == "LegacyFTB") + { + inst.reset(new LegacyFTBInstance(m_globalSettings, m_settings, record.instanceDir)); + } + else if (inst_type == "OneSixFTB") + { + inst.reset(new OneSixFTBInstance(m_globalSettings, m_settings, record.instanceDir)); + } + else + { + return nullptr; + } + qDebug() << "Construction " << record.instanceDir; + + SettingsObject::Lock lock(inst->settings()); + inst->init(); + qDebug() << "Init " << record.instanceDir; + inst->setGroupInitial("FTB"); + /** + * FIXME: this does not respect the user's preferences. BUT, it would work nicely with the planned pack support + * -> instead of changing the user values, change pack values (defaults you can look at and revert to) + */ + /* + inst->setName(record.name); + inst->setIconKey(record.iconKey); + inst->setNotes(record.description); + */ + if (inst->intendedVersionId() != record.mcVersion) + { + inst->setIntendedVersionId(record.mcVersion); + } + qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); + return inst; +} + +InstancePtr FTBInstanceProvider::createInstance(const FTBRecord & record) const +{ + QDir rootDir(record.instanceDir); + + InstancePtr inst; + + qDebug() << "Converting " << record.name << " as new."; + + auto mcVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", record.mcVersion)); + if (!mcVersion) + { + qCritical() << "Can't load instance " << record.instanceDir + << " because minecraft version " << record.mcVersion + << " can't be resolved."; + return nullptr; + } + + if (!rootDir.exists() && !rootDir.mkpath(".")) + { + qCritical() << "Can't create instance folder" << record.instanceDir; + return nullptr; + } + + auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg")); + m_settings->registerSetting("InstanceType", "Legacy"); + + if (mcVersion->usesLegacyLauncher()) + { + m_settings->set("InstanceType", "LegacyFTB"); + inst.reset(new LegacyFTBInstance(m_globalSettings, m_settings, record.instanceDir)); + } + else + { + m_settings->set("InstanceType", "OneSixFTB"); + inst.reset(new OneSixFTBInstance(m_globalSettings, m_settings, record.instanceDir)); + } + // initialize + { + SettingsObject::Lock lock(inst->settings()); + inst->setIntendedVersionId(mcVersion->descriptor()); + inst->init(); + inst->setGroupInitial("FTB"); + inst->setName(record.name); + inst->setIconKey(record.iconKey); + inst->setNotes(record.description); + } + return inst; +} diff --git a/api/logic/minecraft/ftb/FTBInstanceProvider.h b/api/logic/minecraft/ftb/FTBInstanceProvider.h new file mode 100644 index 00000000..fb3ecb6c --- /dev/null +++ b/api/logic/minecraft/ftb/FTBInstanceProvider.h @@ -0,0 +1,45 @@ +#pragma once + +#include "BaseInstanceProvider.h" +#include <QMap> + +class QFileSystemWatcher; + +struct MULTIMC_LOGIC_EXPORT FTBRecord +{ + QString dirName; + QString name; + QString logo; + QString iconKey; + QString mcVersion; + QString description; + QString instanceDir; + QString templateDir; + bool operator==(const FTBRecord other) const + { + return instanceDir == other.instanceDir; + } +}; + +class MULTIMC_LOGIC_EXPORT FTBInstanceProvider : public BaseInstanceProvider +{ + Q_OBJECT + +public: + FTBInstanceProvider (SettingsObjectPtr settings); + +public: + QList<InstanceId> discoverInstances() override; + InstancePtr loadInstance(const InstanceId& id) override; + void loadGroupList() override {}; + void saveGroupList() override {}; + +private: /* methods */ + void discoverFTBEntries(); + InstancePtr createInstance(const FTBRecord & record) const; + InstancePtr loadInstance(const FTBRecord & record) const; + + +private: + QMap<InstanceId, FTBRecord> m_records; +}; diff --git a/api/logic/minecraft/ftb/FTBPlugin.cpp b/api/logic/minecraft/ftb/FTBPlugin.cpp index 134257bf..d14eea63 100644 --- a/api/logic/minecraft/ftb/FTBPlugin.cpp +++ b/api/logic/minecraft/ftb/FTBPlugin.cpp @@ -8,291 +8,9 @@ #include <minecraft/MinecraftVersionList.h> #include <settings/INISettingsObject.h> #include <FileSystem.h> -#include "QDebug" -#include <QXmlStreamReader> -#include <QRegularExpression> - -struct FTBRecord -{ - QString dirName; - QString name; - QString logo; - QString iconKey; - QString mcVersion; - QString description; - QString instanceDir; - QString templateDir; - bool operator==(const FTBRecord other) const - { - return instanceDir == other.instanceDir; - } -}; - -inline uint qHash(FTBRecord record) -{ - return qHash(record.instanceDir); -} - -QSet<FTBRecord> discoverFTBInstances(SettingsObjectPtr globalSettings) -{ - QSet<FTBRecord> records; - QDir dir = QDir(globalSettings->get("FTBLauncherLocal").toString()); - QDir dataDir = QDir(globalSettings->get("FTBRoot").toString()); - if (!dataDir.exists()) - { - qDebug() << "The FTB directory specified does not exist. Please check your settings"; - return records; - } - else if (!dir.exists()) - { - qDebug() << "The FTB launcher data directory specified does not exist. Please check " - "your settings"; - return records; - } - dir.cd("ModPacks"); - auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name); - for (auto filename : allFiles) - { - if (!filename.endsWith(".xml")) - continue; - auto fpath = dir.absoluteFilePath(filename); - QFile f(fpath); - qDebug() << "Discovering FTB instances -- " << fpath; - if (!f.open(QFile::ReadOnly)) - continue; - - // read the FTB packs XML. - QXmlStreamReader reader(&f); - while (!reader.atEnd()) - { - switch (reader.readNext()) - { - case QXmlStreamReader::StartElement: - { - if (reader.name() == "modpack") - { - QXmlStreamAttributes attrs = reader.attributes(); - FTBRecord record; - record.dirName = attrs.value("dir").toString(); - record.instanceDir = dataDir.absoluteFilePath(record.dirName); - record.templateDir = dir.absoluteFilePath(record.dirName); - QDir test(record.instanceDir); - qDebug() << dataDir.absolutePath() << record.instanceDir << record.dirName; - if (!test.exists()) - continue; - record.name = attrs.value("name").toString(); - record.logo = attrs.value("logo").toString(); - QString logo = record.logo; - record.iconKey = logo.remove(QRegularExpression("\\..*")); - auto customVersions = attrs.value("customMCVersions"); - if (!customVersions.isNull()) - { - QMap<QString, QString> versionMatcher; - QString customVersionsStr = customVersions.toString(); - QStringList list = customVersionsStr.split(';'); - for (auto item : list) - { - auto segment = item.split('^'); - if (segment.size() != 2) - { - qCritical() << "FTB: Segment of size < 2 in " - << customVersionsStr; - continue; - } - versionMatcher[segment[0]] = segment[1]; - } - auto actualVersion = attrs.value("version").toString(); - if (versionMatcher.contains(actualVersion)) - { - record.mcVersion = versionMatcher[actualVersion]; - } - else - { - record.mcVersion = attrs.value("mcVersion").toString(); - } - } - else - { - record.mcVersion = attrs.value("mcVersion").toString(); - } - record.description = attrs.value("description").toString(); - records.insert(record); - } - break; - } - case QXmlStreamReader::EndElement: - break; - case QXmlStreamReader::Characters: - break; - default: - break; - } - } - f.close(); - } - return records; -} - -InstancePtr loadInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record) -{ - InstancePtr inst; - - auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg")); - m_settings->registerSetting("InstanceType", "Legacy"); - - qDebug() << "Loading existing " << record.name; - - QString inst_type = m_settings->get("InstanceType").toString(); - if (inst_type == "LegacyFTB") - { - inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir)); - } - else if (inst_type == "OneSixFTB") - { - inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir)); - } - else - { - return nullptr; - } - qDebug() << "Construction " << record.instanceDir; - - SettingsObject::Lock lock(inst->settings()); - inst->init(); - qDebug() << "Init " << record.instanceDir; - inst->setGroupInitial("FTB"); - /** - * FIXME: this does not respect the user's preferences. BUT, it would work nicely with the planned pack support - * -> instead of changing the user values, change pack values (defaults you can look at and revert to) - */ - /* - inst->setName(record.name); - inst->setIconKey(record.iconKey); - inst->setNotes(record.description); - */ - if (inst->intendedVersionId() != record.mcVersion) - { - inst->setIntendedVersionId(record.mcVersion); - } - qDebug() << "Post-Process " << record.instanceDir; - if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap)) - { - return nullptr; - } - qDebug() << "Final " << record.instanceDir; - return inst; -} - -InstancePtr createInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record) -{ - QDir rootDir(record.instanceDir); - - InstancePtr inst; - - qDebug() << "Converting " << record.name << " as new."; - - auto mcVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", record.mcVersion)); - if (!mcVersion) - { - qCritical() << "Can't load instance " << record.instanceDir - << " because minecraft version " << record.mcVersion - << " can't be resolved."; - return nullptr; - } - if (!rootDir.exists() && !rootDir.mkpath(".")) - { - qCritical() << "Can't create instance folder" << record.instanceDir; - return nullptr; - } - - auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg")); - m_settings->registerSetting("InstanceType", "Legacy"); - - if (mcVersion->usesLegacyLauncher()) - { - m_settings->set("InstanceType", "LegacyFTB"); - inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir)); - } - else - { - m_settings->set("InstanceType", "OneSixFTB"); - inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir)); - } - // initialize - { - SettingsObject::Lock lock(inst->settings()); - inst->setIntendedVersionId(mcVersion->descriptor()); - inst->init(); - inst->setGroupInitial("FTB"); - inst->setName(record.name); - inst->setIconKey(record.iconKey); - inst->setNotes(record.description); - qDebug() << "Post-Process " << record.instanceDir; - if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap)) - { - return nullptr; - } - } - return inst; -} - -void FTBPlugin::loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList) -{ - // nothing to load when we don't have - if (globalSettings->get("TrackFTBInstances").toBool() != true) - { - return; - } - - auto records = discoverFTBInstances(globalSettings); - if (!records.size()) - { - qDebug() << "No FTB instances to load."; - return; - } - qDebug() << "Loading FTB instances! -- got " << records.size(); - // process the records we acquired. - for (auto record : records) - { - qDebug() << "Loading FTB instance from " << record.instanceDir; - QString iconKey = record.iconKey; - auto icons = ENV.icons(); - if(icons) - { - icons->addIcon(iconKey, iconKey, FS::PathCombine(record.templateDir, record.logo), IconType::Transient); - } - auto settingsFilePath = FS::PathCombine(record.instanceDir, "instance.cfg"); - qDebug() << "ICON get!"; - - if (QFileInfo(settingsFilePath).exists()) - { - auto instPtr = loadInstance(globalSettings, groupMap, record); - if (!instPtr) - { - qWarning() << "Couldn't load instance config:" << settingsFilePath; - if(!QFile::remove(settingsFilePath)) - { - qWarning() << "Couldn't remove broken instance config!"; - continue; - } - // failed to load, but removed the poisonous file - } - else - { - tempList.append(InstancePtr(instPtr)); - continue; - } - } - auto instPtr = createInstance(globalSettings, groupMap, record); - if (!instPtr) - { - qWarning() << "Couldn't create FTB instance!"; - continue; - } - tempList.append(InstancePtr(instPtr)); - } -} +#include <QDebug> +#include <QRegularExpression> #ifdef Q_OS_WIN32 #include <windows.h> diff --git a/api/logic/minecraft/ftb/FTBPlugin.h b/api/logic/minecraft/ftb/FTBPlugin.h index 6851d8a5..e1b56545 100644 --- a/api/logic/minecraft/ftb/FTBPlugin.h +++ b/api/logic/minecraft/ftb/FTBPlugin.h @@ -9,5 +9,4 @@ class MULTIMC_LOGIC_EXPORT FTBPlugin { public: static void initialize(SettingsObjectPtr globalSettings); - static void loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList); }; diff --git a/api/logic/tasks/ThreadTask.h b/api/logic/tasks/ThreadTask.h index 718dbc91..46ce3a36 100644 --- a/api/logic/tasks/ThreadTask.h +++ b/api/logic/tasks/ThreadTask.h @@ -1,8 +1,9 @@ #pragma once #include "Task.h" +#include "multimc_logic_export.h" -class ThreadTask : public Task +class MULTIMC_LOGIC_EXPORT ThreadTask : public Task { Q_OBJECT public: diff --git a/application/InstanceWindow.cpp b/application/InstanceWindow.cpp index 3bfc78db..9fdd3ca8 100644 --- a/application/InstanceWindow.cpp +++ b/application/InstanceWindow.cpp @@ -95,9 +95,23 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent) connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::on_RunningState_changed); } + + // set up instance destruction detection + { + connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); + } show(); } +void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus) +{ + if(newStatus == BaseInstance::Status::Gone) + { + m_doNotSave = true; + close(); + } +} + void InstanceWindow::setKillButton(bool kill) { if(kill) @@ -145,18 +159,25 @@ void InstanceWindow::on_closeButton_clicked() void InstanceWindow::closeEvent(QCloseEvent *event) { + bool proceed = true; + if(!m_doNotSave) + { + proceed &= m_container->requestClose(event); + } + + if(!proceed) + { + return; + } + MMC->settings()->set("ConsoleWindowState", saveState().toBase64()); MMC->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64()); - - if(m_container->requestClose(event)) + emit isClosing(); + event->accept(); + if(m_shouldQuit) { - emit isClosing(); - event->accept(); - if(m_shouldQuit) - { - // this needs to be delayed so we don't do horrible things - QMetaObject::invokeMethod(MMC, "quit", Qt::QueuedConnection); - } + // this needs to be delayed so we don't do horrible things + QMetaObject::invokeMethod(MMC, "quit", Qt::QueuedConnection); } } diff --git a/application/InstanceWindow.h b/application/InstanceWindow.h index 1da2231f..71bf4d40 100644 --- a/application/InstanceWindow.h +++ b/application/InstanceWindow.h @@ -55,6 +55,7 @@ slots: void on_InstanceLaunchTask_changed(std::shared_ptr<LaunchTask> proc); void on_RunningState_changed(bool running); + void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); protected: void closeEvent(QCloseEvent *) override; @@ -67,6 +68,7 @@ private: unique_qobject_ptr<LaunchController> m_launchController; InstancePtr m_instance; bool m_shouldQuit = false; + bool m_doNotSave = false; PageContainer *m_container = nullptr; QPushButton *m_closeButton = nullptr; QPushButton *m_killButton = nullptr; diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index c91f5256..3a297784 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -88,6 +88,8 @@ #include "dialogs/EditAccountDialog.h" #include "dialogs/NotificationDialog.h" #include "dialogs/ExportInstanceDialog.h" +#include <FolderInstanceProvider.h> +#include <InstanceImportTask.h> class MainWindow::Ui { @@ -996,26 +998,6 @@ void MainWindow::setCatBackground(bool enabled) } } -static QFileInfo findRecursive(const QString &dir, const QString &name) -{ - for (const auto info : QDir(dir).entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files, QDir::DirsLast)) - { - if (info.isFile() && info.fileName() == name) - { - return info; - } - else if (info.isDir()) - { - const QFileInfo res = findRecursive(info.absoluteFilePath(), name); - if (res.isFile() && res.exists()) - { - return res; - } - } - } - return QFileInfo(); -} - // FIXME: eliminate, should not be needed void MainWindow::waitForMinecraftVersions() { @@ -1028,147 +1010,50 @@ void MainWindow::waitForMinecraftVersions() } } -InstancePtr MainWindow::instanceFromZipPack(QString instName, QString instGroup, QString instIcon, QUrl url) +void MainWindow::runModalTask(Task *task) { - InstancePtr newInstance; - - QString instancesDir = MMC->settings()->get("InstanceDir").toString(); - QString instDirName = FS::DirNameFromString(instName, instancesDir); - QString instDir = FS::PathCombine(instancesDir, instDirName); - - QString archivePath; - if (url.isLocalFile()) - { - archivePath = url.toLocalFile(); - } - else - { - const QString path = url.host() + '/' + url.path(); - auto entry = ENV.metacache()->resolveEntry("general", path); - entry->setStale(true); - NetJob job(tr("Modpack download")); - job.addNetAction(Net::Download::makeCached(url, entry)); - - // FIXME: possibly causes endless loop problems - ProgressDialog dlDialog(this); - job.setStatus(tr("Downloading modpack:\n%1").arg(url.toString())); - if (dlDialog.execWithTask(&job) != QDialog::Accepted) + connect(task, &Task::failed, [this](QString reason) { - return nullptr; - } - archivePath = entry->getFullPath(); - } + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Warning)->show(); + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); +} - QTemporaryDir extractTmpDir; - QDir extractDir(extractTmpDir.path()); - qDebug() << "Attempting to create instance from" << archivePath; - if (MMCZip::extractDir(archivePath, extractDir.absolutePath()).isEmpty()) - { - CustomMessageBox::selectable(this, tr("Error"), tr("Failed to extract modpack"), QMessageBox::Warning)->show(); - return nullptr; - } - const QFileInfo instanceCfgFile = findRecursive(extractDir.absolutePath(), "instance.cfg"); - if (!instanceCfgFile.isFile() || !instanceCfgFile.exists()) - { - CustomMessageBox::selectable(this, tr("Error"), tr("Archive does not contain instance.cfg"))->show(); - return nullptr; - } - if (!FS::copy(instanceCfgFile.absoluteDir().absolutePath(), instDir)()) - { - CustomMessageBox::selectable(this, tr("Error"), tr("Unable to copy instance"))->show(); - return nullptr; - } +void MainWindow::instanceFromZipPack(QString instName, QString instGroup, QString instIcon, QUrl url) +{ + std::unique_ptr<Task> task(MMC->folderProvider()->zipImportTask(url, instName, instGroup, instIcon)); + runModalTask(task.get()); - auto error = MMC->instances()->loadInstance(newInstance, instDir); - QString errorMsg = tr("Failed to load instance %1: ").arg(instDirName); - switch (error) - { - case InstanceList::UnknownLoadError: - errorMsg += tr("Unkown error"); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - return nullptr; - case InstanceList::NotAnInstance: - errorMsg += tr("Not an instance"); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - return nullptr; - default: - break; - } + // FIXME: handle instance selection after creation + // finalizeInstance(newInstance); +} - newInstance->setName(instName); - if (instIcon != "default") - { - newInstance->setIconKey(instIcon); - } - else - { - instIcon = newInstance->iconKey(); - auto importIconPath = FS::PathCombine(newInstance->instanceRoot(), instIcon + ".png"); - if (QFile::exists(importIconPath)) - { - // import icon - auto iconList = MMC->icons(); - // FIXME: check if the file is OK before removing the existing one... - if (iconList->iconFileExists(instIcon)) - { - // FIXME: ask if icon should be overwritten. Show difference in the question dialog. - iconList->deleteIcon(instIcon); - } - iconList->installIcons({importIconPath}); - } - } - newInstance->setGroupInitial(instGroup); - // reset time played on import... because packs. - newInstance->resetTimePlayed(); - MMC->instances()->add(InstancePtr(newInstance)); - MMC->instances()->saveGroupList(); +void MainWindow::instanceFromVersion(QString instName, QString instGroup, QString instIcon, BaseVersionPtr version) +{ + std::unique_ptr<Task> task(MMC->folderProvider()->creationTask(version, instName, instGroup, instIcon)); + runModalTask(task.get()); - finalizeInstance(newInstance); - return newInstance; + // FIXME: handle instance selection after creation + // finalizeInstance(newInstance); } -InstancePtr MainWindow::instanceFromVersion(QString instName, QString instGroup, QString instIcon, BaseVersionPtr version) +void MainWindow::on_actionCopyInstance_triggered() { - InstancePtr newInstance; - - QString instancesDir = MMC->settings()->get("InstanceDir").toString(); - QString instDirName = FS::DirNameFromString(instName, instancesDir); - QString instDir = FS::PathCombine(instancesDir, instDirName); - auto error = MMC->instances()->createInstance(newInstance, version, instDir); - QString errorMsg = tr("Failed to create instance %1: ").arg(instDirName); - switch (error) - { - case InstanceList::NoCreateError: - break; + if (!m_selectedInstance) + return; - case InstanceList::InstExists: - { - errorMsg += tr("An instance with the given directory name already exists."); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - return nullptr; - } + CopyInstanceDialog copyInstDlg(m_selectedInstance, this); + if (!copyInstDlg.exec()) + return; - case InstanceList::CantCreateDir: - { - errorMsg += tr("Failed to create the instance directory."); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - return nullptr; - } + std::unique_ptr<Task> task(MMC->folderProvider()->copyTask(m_selectedInstance, copyInstDlg.instName(), copyInstDlg.instGroup(), + copyInstDlg.iconKey(), copyInstDlg.shouldCopySaves())); + runModalTask(task.get()); - default: - { - errorMsg += tr("Unknown instance loader error %1").arg(error); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - return nullptr; - } - } - newInstance->setName(instName); - newInstance->setIconKey(instIcon); - newInstance->setGroupInitial(instGroup); - MMC->instances()->add(InstancePtr(newInstance)); - MMC->instances()->saveGroupList(); - finalizeInstance(newInstance); - return newInstance; + // FIXME: handle instance selection after creation + // finalizeInstance(newInstance); } void MainWindow::finalizeInstance(InstancePtr inst) @@ -1251,56 +1136,6 @@ void MainWindow::on_actionDISCORD_triggered() DesktopServices::openUrl(QUrl("https://discord.gg/0k2zsXGNHs0fE4Wm")); } -void MainWindow::on_actionCopyInstance_triggered() -{ - if (!m_selectedInstance) - return; - - CopyInstanceDialog copyInstDlg(m_selectedInstance, this); - if (!copyInstDlg.exec()) - return; - - QString instancesDir = MMC->settings()->get("InstanceDir").toString(); - QString instDirName = FS::DirNameFromString(copyInstDlg.instName(), instancesDir); - QString instDir = FS::PathCombine(instancesDir, instDirName); - bool copySaves = copyInstDlg.shouldCopySaves(); - - InstancePtr newInstance; - auto error = MMC->instances()->copyInstance(newInstance, m_selectedInstance, instDir, copySaves); - - QString errorMsg = tr("Failed to create instance %1: ").arg(instDirName); - switch (error) - { - case InstanceList::NoCreateError: - newInstance->setName(copyInstDlg.instName()); - newInstance->setIconKey(copyInstDlg.iconKey()); - MMC->instances()->add(newInstance); - newInstance->setGroupPost(copyInstDlg.instGroup()); - return; - - case InstanceList::InstExists: - { - errorMsg += tr("An instance with the given directory name already exists."); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - break; - } - - case InstanceList::CantCreateDir: - { - errorMsg += tr("Failed to create the instance directory."); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - break; - } - - default: - { - errorMsg += tr("Unknown instance loader error %1").arg(error); - CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - break; - } - } -} - void MainWindow::on_actionChangeInstIcon_triggered() { if (!m_selectedInstance) @@ -1386,7 +1221,7 @@ void MainWindow::on_actionViewInstanceFolder_triggered() void MainWindow::on_actionRefresh_triggered() { - MMC->instances()->loadList(); + MMC->instances()->loadList(true); } void MainWindow::on_actionViewCentralModsFolder_triggered() diff --git a/application/MainWindow.h b/application/MainWindow.h index 3f0ec6e6..d0660676 100644 --- a/application/MainWindow.h +++ b/application/MainWindow.h @@ -170,8 +170,9 @@ private: void setSelectedInstanceById(const QString &id); void waitForMinecraftVersions(); - InstancePtr instanceFromVersion(QString instName, QString instGroup, QString instIcon, BaseVersionPtr version); - InstancePtr instanceFromZipPack(QString instName, QString instGroup, QString instIcon, QUrl url); + void runModalTask(Task *task); + void instanceFromVersion(QString instName, QString instGroup, QString instIcon, BaseVersionPtr version); + void instanceFromZipPack(QString instName, QString instGroup, QString instIcon, QUrl url); void finalizeInstance(InstancePtr inst); void launch(InstancePtr instance, bool online = true, BaseProfilerFactory *profiler = nullptr); diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index 6d671bd4..a226f3b7 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -25,6 +25,9 @@ #include <QStyleFactory> #include "InstanceList.h" +#include "FolderInstanceProvider.h" +#include "minecraft/ftb/FTBInstanceProvider.h" + #include <minecraft/auth/MojangAccountList.h> #include "icons/IconList.h" //FIXME: get rid of this @@ -261,10 +264,13 @@ MultiMC::MultiMC(int &argc, char **argv, bool test_mode) : QApplication(argc, ar << "Your instance path contains \'!\' and this is known to cause java problems"; } m_instances.reset(new InstanceList(m_settings, InstDirSetting->get().toString(), this)); + m_instanceFolder = new FolderInstanceProvider(m_settings, instDir); + connect(InstDirSetting.get(), &Setting::SettingChanged, m_instanceFolder, &FolderInstanceProvider::on_InstFolderChanged); + m_instances->addInstanceProvider(m_instanceFolder); + m_instances->addInstanceProvider(new FTBInstanceProvider(m_settings)); + qDebug() << "Loading Instances..."; - m_instances->loadList(); - connect(InstDirSetting.get(), SIGNAL(SettingChanged(const Setting &, QVariant)), - m_instances.get(), SLOT(on_InstFolderChanged(const Setting &, QVariant))); + m_instances->loadList(true); // and accounts m_accounts.reset(new MojangAccountList(this)); @@ -1007,7 +1013,7 @@ void MultiMC::onExit() { if(m_instances) { - m_instances->saveGroupList(); + // m_instances->saveGroupList(); } ENV.destroy(); if(logFile) diff --git a/application/MultiMC.h b/application/MultiMC.h index 3b8751c1..02038578 100644 --- a/application/MultiMC.h +++ b/application/MultiMC.h @@ -7,6 +7,7 @@ #include <QIcon> #include <QDateTime> #include <updater/GoUpdate.h> +class FolderInstanceProvider; class GenericPageProvider; class QFile; @@ -91,6 +92,11 @@ public: return m_instances; } + FolderInstanceProvider * folderProvider() + { + return m_instanceFolder; + } + std::shared_ptr<IconList> icons() { return m_icons; @@ -164,6 +170,7 @@ private: std::shared_ptr<QTranslator> m_mmc_translator; std::shared_ptr<SettingsObject> m_settings; std::shared_ptr<InstanceList> m_instances; + FolderInstanceProvider * m_instanceFolder; std::shared_ptr<IconList> m_icons; std::shared_ptr<UpdateChecker> m_updateChecker; std::shared_ptr<MojangAccountList> m_accounts; |