diff --git a/api/logic/BaseInstance.cpp b/api/logic/BaseInstance.cpp index f52b2812..027b8f78 100644 --- a/api/logic/BaseInstance.cpp +++ b/api/logic/BaseInstance.cpp @@ -197,7 +197,7 @@ bool BaseInstance::canLaunch() const return (!hasVersionBroken() && !isRunning()); } -bool BaseInstance::reload() +bool BaseInstance::reloadSettings() { return m_settings->reload(); } diff --git a/api/logic/BaseInstance.h b/api/logic/BaseInstance.h index 26d4bc35..d0909031 100644 --- a/api/logic/BaseInstance.h +++ b/api/logic/BaseInstance.h @@ -30,6 +30,8 @@ #include "MessageLevel.h" #include "pathmatcher/IPathMatcher.h" +#include "net/Mode.h" + #include "multimc_logic_export.h" class QDir; @@ -148,7 +150,7 @@ public: virtual SettingsObjectPtr settings() const; /// returns a valid update task - virtual shared_qobject_ptr createUpdateTask() = 0; + virtual shared_qobject_ptr createUpdateTask(Net::Mode mode) = 0; /// returns a valid launcher (task container) virtual std::shared_ptr createLaunchTask(AuthSessionPtr account) = 0; @@ -224,7 +226,7 @@ public: virtual bool canEdit() const = 0; virtual bool canExport() const = 0; - virtual bool reload(); + bool reloadSettings(); /** * 'print' a verbose desription of the instance into a QStringList diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 42bea98c..8a8ef495 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -239,8 +239,14 @@ set(MINECRAFT_SOURCES minecraft/MinecraftInstance.h minecraft/LaunchProfile.cpp minecraft/LaunchProfile.h + minecraft/Component.cpp + minecraft/Component.h minecraft/ComponentList.cpp minecraft/ComponentList.h + minecraft/ComponentUpdateTask.cpp + minecraft/ComponentUpdateTask.h + minecraft/MinecraftLoadAndCheck.h + minecraft/MinecraftLoadAndCheck.cpp minecraft/MinecraftUpdate.h minecraft/MinecraftUpdate.cpp minecraft/MojangVersionFormat.cpp @@ -260,8 +266,6 @@ set(MINECRAFT_SOURCES minecraft/MojangDownloadInfo.h minecraft/VersionFile.cpp minecraft/VersionFile.h - minecraft/ProfilePatch.cpp - minecraft/ProfilePatch.h minecraft/VersionFilterData.h minecraft/VersionFilterData.cpp minecraft/Mod.h diff --git a/api/logic/InstanceCreationTask.cpp b/api/logic/InstanceCreationTask.cpp index 21892f82..8a68815a 100644 --- a/api/logic/InstanceCreationTask.cpp +++ b/api/logic/InstanceCreationTask.cpp @@ -5,6 +5,7 @@ //FIXME: remove this #include "minecraft/MinecraftInstance.h" +#include "minecraft/ComponentList.h" InstanceCreationTask::InstanceCreationTask(SettingsObjectPtr settings, const QString & stagingPath, BaseVersionPtr version, const QString& instName, const QString& instIcon, const QString& instGroup) @@ -25,11 +26,13 @@ void InstanceCreationTask::executeTask() instanceSettings->suspendSave(); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); - auto inst = new MinecraftInstance(m_globalSettings, instanceSettings, m_stagingPath); - inst->setComponentVersion("net.minecraft", m_version->descriptor()); - inst->setName(m_instName); - inst->setIconKey(m_instIcon); - inst->init(); + MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); + auto components = inst.getComponentList(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + inst.setName(m_instName); + inst.setIconKey(m_instIcon); + inst.init(); instanceSettings->resumeSave(); } emitSucceeded(); diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index e2782cd8..80f68458 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -242,7 +242,9 @@ void InstanceImportTask::processFlame() mcVersion.remove(QRegExp("[.]+$")); qWarning() << "Mysterious trailing dots removed from Minecraft version while importing pack."; } - instance.setComponentVersion("net.minecraft", mcVersion); + auto components = instance.getComponentList(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", mcVersion, true); if(!forgeVersion.isEmpty()) { // FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata. @@ -257,7 +259,7 @@ void InstanceImportTask::processFlame() qWarning() << "Could not map recommended forge version for" << mcVersion; } } - instance.setComponentVersion("net.minecraftforge", forgeVersion); + components->setComponentVersion("net.minecraftforge", forgeVersion); } if (m_instIcon != "default") { diff --git a/api/logic/NullInstance.h b/api/logic/NullInstance.h index f689c5ab..27a8a251 100644 --- a/api/logic/NullInstance.h +++ b/api/logic/NullInstance.h @@ -29,7 +29,7 @@ public: { return nullptr; } - virtual shared_qobject_ptr< Task > createUpdateTask() override + virtual shared_qobject_ptr< Task > createUpdateTask(Net::Mode mode) override { return nullptr; } diff --git a/api/logic/ProblemProvider.h b/api/logic/ProblemProvider.h index 4040f4fa..978710f0 100644 --- a/api/logic/ProblemProvider.h +++ b/api/logic/ProblemProvider.h @@ -1,5 +1,7 @@ #pragma once +#include "multimc_logic_export.h" + enum class ProblemSeverity { None, @@ -13,7 +15,7 @@ struct PatchProblem QString m_description; }; -class ProblemProvider +class MULTIMC_LOGIC_EXPORT ProblemProvider { public: virtual ~ProblemProvider() {}; @@ -21,7 +23,7 @@ public: virtual ProblemSeverity getProblemSeverity() const = 0; }; -class ProblemContainer : public ProblemProvider +class MULTIMC_LOGIC_EXPORT ProblemContainer : public ProblemProvider { public: const QList getProblems() const override diff --git a/api/logic/launch/steps/Update.cpp b/api/logic/launch/steps/Update.cpp index f06c39eb..42ca3c67 100644 --- a/api/logic/launch/steps/Update.cpp +++ b/api/logic/launch/steps/Update.cpp @@ -23,7 +23,7 @@ void Update::executeTask() emitFailed(tr("Task aborted.")); return; } - m_updateTask.reset(m_parent->instance()->createUpdateTask()); + m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode)); if(m_updateTask) { connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished())); diff --git a/api/logic/launch/steps/Update.h b/api/logic/launch/steps/Update.h index d855a1db..e9573bcd 100644 --- a/api/logic/launch/steps/Update.h +++ b/api/logic/launch/steps/Update.h @@ -19,13 +19,14 @@ #include #include #include +#include // FIXME: stupid. should be defined by the instance type? or even completely abstracted away... class Update: public LaunchStep { Q_OBJECT public: - explicit Update(LaunchTask *parent):LaunchStep(parent) {}; + explicit Update(LaunchTask *parent, Net::Mode mode):LaunchStep(parent), m_mode(mode) {}; virtual ~Update() {}; void executeTask() override; @@ -40,4 +41,5 @@ private slots: private: shared_qobject_ptr m_updateTask; bool m_aborted = false; + Net::Mode m_mode = Net::Mode::Offline; }; diff --git a/api/logic/meta/BaseEntity.cpp b/api/logic/meta/BaseEntity.cpp index 6bac71ee..7cf327be 100644 --- a/api/logic/meta/BaseEntity.cpp +++ b/api/logic/meta/BaseEntity.cpp @@ -99,7 +99,7 @@ bool Meta::BaseEntity::loadLocalFile() } } -void Meta::BaseEntity::load() +void Meta::BaseEntity::load(Net::Mode loadType) { // load local file if nothing is loaded yet if(!isLoaded()) @@ -110,7 +110,7 @@ void Meta::BaseEntity::load() } } // if we need remote update, run the update task - if(!shouldStartRemoteUpdate()) + if(loadType == Net::Mode::Offline || !shouldStartRemoteUpdate()) { return; } diff --git a/api/logic/meta/BaseEntity.h b/api/logic/meta/BaseEntity.h index 4483beab..4c74b1a7 100644 --- a/api/logic/meta/BaseEntity.h +++ b/api/logic/meta/BaseEntity.h @@ -20,6 +20,7 @@ #include "QObjectPtr.h" #include "multimc_logic_export.h" +#include "net/Mode.h" class Task; namespace Meta @@ -54,7 +55,7 @@ public: bool isLoaded() const; bool shouldStartRemoteUpdate() const; - void load(); + void load(Net::Mode loadType); shared_qobject_ptr getCurrentTask(); protected: /* methods */ diff --git a/api/logic/meta/JsonFormat.cpp b/api/logic/meta/JsonFormat.cpp index d13c89df..2c313478 100644 --- a/api/logic/meta/JsonFormat.cpp +++ b/api/logic/meta/JsonFormat.cpp @@ -51,18 +51,11 @@ static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj) version->setType(ensureString(obj, "type", QString())); version->setParentUid(ensureString(obj, "parentUid", QString())); version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); - if(obj.contains("requires")) - { - QHash requires; - auto reqobj = requireObject(obj, "requires"); - auto iter = reqobj.begin(); - while(iter != reqobj.end()) - { - requires[iter.key()] = requireString(iter.value()); - iter++; - } - version->setRequires(requires); - } + version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); + RequireSet requires, conflicts; + parseRequires(obj, &requires, "requires"); + parseRequires(obj, &conflicts, "conflicts"); + version->setRequires(requires, conflicts); return version; } @@ -145,4 +138,53 @@ void parseVersion(const QJsonObject &obj, Version *ptr) throw ParseException(QObject::tr("Unknown formatVersion: %1").arg(version)); } } + +/* +[ +{"uid":"foo", "equals":"version"} +] +*/ +void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName) +{ + if(obj.contains(keyName)) + { + QSet requires; + auto reqArray = requireArray(obj, keyName); + auto iter = reqArray.begin(); + while(iter != reqArray.end()) + { + auto reqObject = requireObject(*iter); + auto uid = requireString(reqObject, "uid"); + auto equals = ensureString(reqObject, "equals", QString()); + auto suggests = ensureString(reqObject, "suggests", QString()); + ptr->insert({uid, equals, suggests}); + iter++; + } + } } +void serializeRequires(QJsonObject& obj, RequireSet* ptr, const char * keyName) +{ + if(!ptr || ptr->empty()) + { + return; + } + QJsonArray arrOut; + for(auto &iter: *ptr) + { + QJsonObject reqOut; + reqOut.insert("uid", iter.uid); + if(!iter.equalsVersion.isEmpty()) + { + reqOut.insert("equals", iter.equalsVersion); + } + if(!iter.suggests.isEmpty()) + { + reqOut.insert("suggests", iter.suggests); + } + arrOut.append(reqOut); + } + obj.insert(keyName, arrOut); +} + +} + diff --git a/api/logic/meta/JsonFormat.h b/api/logic/meta/JsonFormat.h index aaed07fc..9b9dcd91 100644 --- a/api/logic/meta/JsonFormat.h +++ b/api/logic/meta/JsonFormat.h @@ -20,6 +20,7 @@ #include "Exception.h" #include "meta/BaseEntity.h" +#include namespace Meta { @@ -32,9 +33,41 @@ class ParseException : public Exception public: using Exception::Exception; }; +struct Require +{ + bool operator==(const Require & rhs) const + { + return uid == rhs.uid; + } + bool operator<(const Require & rhs) const + { + return uid < rhs.uid; + } + bool deepEquals(const Require & rhs) const + { + return uid == rhs.uid + && equalsVersion == rhs.equalsVersion + && suggests == rhs.suggests; + } + QString uid; + QString equalsVersion; + QString suggests; +}; + +inline Q_DECL_PURE_FUNCTION uint qHash(const Require &key, uint seed = 0) Q_DECL_NOTHROW +{ + return qHash(key.uid, seed); +} + +using RequireSet = std::set; void parseIndex(const QJsonObject &obj, Index *ptr); void parseVersion(const QJsonObject &obj, Version *ptr); void parseVersionList(const QJsonObject &obj, VersionList *ptr); +// FIXME: this has a different shape than the others...FIX IT!? +void parseRequires(const QJsonObject &obj, RequireSet * ptr, const char * keyName = "requires"); +void serializeRequires(QJsonObject & objOut, RequireSet* ptr, const char * keyName = "requires"); } + +Q_DECLARE_METATYPE(std::set); \ No newline at end of file diff --git a/api/logic/meta/Version.cpp b/api/logic/meta/Version.cpp index bf739157..d82878a1 100644 --- a/api/logic/meta/Version.cpp +++ b/api/logic/meta/Version.cpp @@ -74,12 +74,20 @@ void Meta::Version::merge(const std::shared_ptr &other) } if (m_requires != version->m_requires) { - setRequires(version->m_requires); + m_requires = version->m_requires; + } + if (m_conflicts != version->m_conflicts) + { + m_conflicts = version->m_conflicts; } if (m_parentUid != version->m_parentUid) { setParentUid(version->m_parentUid); } + if(m_volatile != version->m_volatile) + { + setVolatile(version->m_volatile); + } if(version->m_data) { setData(version->m_data); @@ -109,12 +117,19 @@ void Meta::Version::setTime(const qint64 time) emit timeChanged(); } -void Meta::Version::setRequires(const QHash &requires) +void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts) { m_requires = requires; + m_conflicts = conflicts; emit requiresChanged(); } +void Meta::Version::setVolatile(bool volatile_) +{ + m_volatile = volatile_; +} + + void Meta::Version::setData(const VersionFilePtr &data) { m_data = data; diff --git a/api/logic/meta/Version.h b/api/logic/meta/Version.h index 2f92ee9f..61f253c6 100644 --- a/api/logic/meta/Version.h +++ b/api/logic/meta/Version.h @@ -28,6 +28,8 @@ #include "multimc_logic_export.h" +#include "JsonFormat.h" + namespace Meta { using VersionPtr = std::shared_ptr; @@ -65,7 +67,7 @@ public: /* con/des */ { return m_time; } - const QHash &requires() const + const Meta::RequireSet &requires() const { return m_requires; } @@ -77,6 +79,10 @@ public: /* con/des */ { return m_recommended; } + bool isLoaded() const + { + return m_data != nullptr; + } void merge(const std::shared_ptr &other) override; void parse(const QJsonObject &obj) override; @@ -87,7 +93,8 @@ public: // for usage by format parsers only void setParentUid(const QString &parentUid); void setType(const QString &type); void setTime(const qint64 time); - void setRequires(const QHash &requires); + void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts); + void setVolatile(bool volatile_); void setRecommended(bool recommended); void setProvidesRecommendations(); void setData(const VersionFilePtr &data); @@ -106,7 +113,9 @@ private: QString m_version; QString m_type; qint64 m_time = 0; - QHash m_requires; + Meta::RequireSet m_requires; + Meta::RequireSet m_conflicts; + bool m_volatile = false; VersionFilePtr m_data; }; } diff --git a/api/logic/meta/VersionList.cpp b/api/logic/meta/VersionList.cpp index 4da0fb76..8910c4d7 100644 --- a/api/logic/meta/VersionList.cpp +++ b/api/logic/meta/VersionList.cpp @@ -30,7 +30,7 @@ VersionList::VersionList(const QString &uid, QObject *parent) shared_qobject_ptr VersionList::getLoadTask() { - load(); + load(Net::Mode::Online); return getCurrentTask(); } @@ -81,10 +81,13 @@ QVariant VersionList::data(const QModelIndex &index, int role) const return QVariant(); } auto & reqs = version->requires(); - auto iter = reqs.find(parentUid); + auto iter = std::find_if(reqs.begin(), reqs.end(), [&parentUid](const Require & req) + { + return req.uid == parentUid; + }); if (iter != reqs.end()) { - return iter.value(); + return (*iter).equalsVersion; } } case TypeRole: return version->type(); @@ -159,6 +162,7 @@ void VersionList::setVersions(const QVector &versions) setupAddedVersion(i, m_versions.at(i)); } + // FIXME: this is dumb, we have 'recommended' as part of the metadata already... auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const VersionPtr &ptr) { return ptr->type() == "release"; }); m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt; endResetModel(); @@ -169,6 +173,22 @@ void VersionList::parse(const QJsonObject& obj) parseVersionList(obj, this); } +// FIXME: this is dumb, we have 'recommended' as part of the metadata already... +static const Meta::VersionPtr &getBetterVersion(const Meta::VersionPtr &a, const Meta::VersionPtr &b) +{ + if(!a) + return b; + if(!b) + return a; + if(a->type() == b->type()) + { + // newer of same type wins + return (a->rawTime() > b->rawTime() ? a : b); + } + // 'release' type wins + return (a->type() == "release" ? a : b); +} + void VersionList::merge(const BaseEntity::Ptr &other) { const VersionListPtr list = std::dynamic_pointer_cast(other); @@ -199,10 +219,7 @@ void VersionList::merge(const BaseEntity::Ptr &other) // connect it. setupAddedVersion(m_versions.size(), version); m_versions.append(version); - if (!m_recommended || (version->type() == "release" && version->rawTime() > m_recommended->rawTime())) - { - m_recommended = version; - } + m_recommended = getBetterVersion(m_recommended, version); } endResetModel(); } diff --git a/api/logic/minecraft/Component.cpp b/api/logic/minecraft/Component.cpp new file mode 100644 index 00000000..db523142 --- /dev/null +++ b/api/logic/minecraft/Component.cpp @@ -0,0 +1,408 @@ +#include +#include +#include +#include "Component.h" + +#include "meta/Version.h" +#include "VersionFile.h" +#include "minecraft/ComponentList.h" +#include +#include +#include "OneSixVersionFormat.h" +#include + +Component::Component(ComponentList * parent, const QString& uid) +{ + assert(parent); + m_parent = parent; + + m_uid = uid; +} + +Component::Component(ComponentList * parent, std::shared_ptr version) +{ + assert(parent); + m_parent = parent; + + m_metaVersion = version; + m_uid = version->uid(); + m_version = m_cachedVersion = version->version(); + m_cachedName = version->name(); + m_loaded = version->isLoaded(); +} + +Component::Component(ComponentList * parent, const QString& uid, std::shared_ptr file) +{ + assert(parent); + m_parent = parent; + + m_file = file; + m_uid = uid; + m_cachedVersion = m_file->version; + m_cachedName = m_file->name; + m_loaded = true; +} + +std::shared_ptr Component::getMeta() +{ + return m_metaVersion; +} + +void Component::applyTo(LaunchProfile* profile) +{ + auto vfile = getVersionFile(); + if(vfile) + { + vfile->applyTo(profile); + } + else + { + profile->applyProblemSeverity(getProblemSeverity()); + } +} + +std::shared_ptr Component::getVersionFile() const +{ + if(m_metaVersion) + { + if(!m_metaVersion->isLoaded()) + { + m_metaVersion->load(Net::Mode::Online); + } + return m_metaVersion->data(); + } + else + { + return m_file; + } +} + +std::shared_ptr Component::getVersionList() const +{ + // FIXME: what if the metadata index isn't loaded yet? + if(ENV.metadataIndex()->hasUid(m_uid)) + { + return ENV.metadataIndex()->get(m_uid); + } + return nullptr; +} + +int Component::getOrder() +{ + if(m_orderOverride) + return m_order; + + auto vfile = getVersionFile(); + if(vfile) + { + return vfile->order; + } + return 0; +} +void Component::setOrder(int order) +{ + m_orderOverride = true; + m_order = order; +} +QString Component::getID() +{ + return m_uid; +} +QString Component::getName() +{ + if (!m_cachedName.isEmpty()) + return m_cachedName; + return m_uid; +} +QString Component::getVersion() +{ + return m_cachedVersion; +} +QString Component::getFilename() +{ + return m_parent->patchFilePathForUid(m_uid); +} +QDateTime Component::getReleaseDateTime() +{ + if(m_metaVersion) + { + return m_metaVersion->time(); + } + auto vfile = getVersionFile(); + if(vfile) + { + return vfile->releaseTime; + } + // FIXME: fake + return QDateTime::currentDateTime(); +} + +bool Component::isCustom() +{ + return m_file != nullptr; +}; + +bool Component::isCustomizable() +{ + if(m_metaVersion) + { + if(getVersionFile()) + { + return true; + } + } + return false; +} +bool Component::isRemovable() +{ + return !m_important; +} +bool Component::isRevertible() +{ + if (isCustom()) + { + if(ENV.metadataIndex()->hasUid(m_uid)) + { + return true; + } + } + return false; +} +bool Component::isMoveable() +{ + // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'. + return true; +} +bool Component::isVersionChangeable() +{ + auto list = getVersionList(); + if(list) + { + if(!list->isLoaded()) + { + list->load(Net::Mode::Online); + } + return list->count() != 0; + } + return false; +} + +void Component::setImportant(bool state) +{ + if(m_important != state) + { + m_important = state; + emit dataChanged(); + } +} + +ProblemSeverity Component::getProblemSeverity() const +{ + auto file = getVersionFile(); + if(file) + { + return file->getProblemSeverity(); + } + return ProblemSeverity::Error; +} + +const QList Component::getProblems() const +{ + auto file = getVersionFile(); + if(file) + { + return file->getProblems(); + } + return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}}; +} + +void Component::setVersion(const QString& version) +{ + if(version == m_version) + { + return; + } + m_version = version; + if(m_loaded) + { + // we are loaded and potentially have state to invalidate + if(m_file) + { + // we have a file... explicit version has been changed and there is nothing else to do. + } + else + { + // we don't have a file, therefore we are loaded with metadata + m_cachedVersion = version; + // see if the meta version is loaded + auto metaVersion = ENV.metadataIndex()->get(m_uid, version); + if(metaVersion->isLoaded()) + { + // if yes, we can continue with that. + m_metaVersion = metaVersion; + } + else + { + // if not, we need loading + m_metaVersion.reset(); + m_loaded = false; + } + updateCachedData(); + } + } + else + { + // not loaded... assume it will be sorted out later by the update task + } + emit dataChanged(); +} + +bool Component::customize() +{ + if(isCustom()) + { + return false; + } + + auto filename = getFilename(); + if(!FS::ensureFilePathExists(filename)) + { + return false; + } + // FIXME: get rid of this try-catch. + try + { + QSaveFile jsonFile(filename); + if(!jsonFile.open(QIODevice::WriteOnly)) + { + return false; + } + auto vfile = getVersionFile(); + if(!vfile) + { + return false; + } + auto document = OneSixVersionFormat::versionFileToJson(vfile); + jsonFile.write(document.toJson()); + if(!jsonFile.commit()) + { + return false; + } + m_file = vfile; + m_metaVersion.reset(); + emit dataChanged(); + } + catch (Exception &error) + { + qWarning() << "Version could not be loaded:" << error.cause(); + } + return true; +} + +bool Component::revert() +{ + if(!isCustom()) + { + // already not custom + return true; + } + auto filename = getFilename(); + bool result = true; + // just kill the file and reload + if(QFile::exists(filename)) + { + result = QFile::remove(filename); + } + if(result) + { + // file gone... + m_file.reset(); + + // check local cache for metadata... + auto version = ENV.metadataIndex()->get(m_uid, m_version); + if(version->isLoaded()) + { + m_metaVersion = version; + } + else + { + m_metaVersion.reset(); + m_loaded = false; + } + emit dataChanged(); + } + return result; +} + +/** + * deep inspecting compare for requirement sets + * By default, only uids are compared for set operations. + * This compares all fields of the Require structs in the sets. + */ +static bool deepCompare(const std::set & a, const std::set & b) +{ + // NOTE: this needs to be rewritten if the type of Meta::RequireSet changes + if(a.size() != b.size()) + { + return false; + } + for(const auto & reqA :a) + { + const auto &iter2 = b.find(reqA); + if(iter2 == b.cend()) + { + return false; + } + const auto & reqB = *iter2; + if(!reqA.deepEquals(reqB)) + { + return false; + } + } + return true; +} + +void Component::updateCachedData() +{ + auto file = getVersionFile(); + if(file) + { + bool changed = false; + if(m_cachedName != file->name) + { + m_cachedName = file->name; + changed = true; + } + if(m_cachedVersion != file->version) + { + m_cachedVersion = file->version; + changed = true; + } + if(m_cachedVolatile != file->m_volatile) + { + m_cachedVolatile = file->m_volatile; + changed = true; + } + if(!deepCompare(m_cachedRequires, file->requires)) + { + m_cachedRequires = file->requires; + changed = true; + } + if(!deepCompare(m_cachedConflicts, file->conflicts)) + { + m_cachedConflicts = file->conflicts; + changed = true; + } + if(changed) + { + emit dataChanged(); + } + } + else + { + // in case we removed all the metadata + m_cachedRequires.clear(); + m_cachedConflicts.clear(); + emit dataChanged(); + } +} diff --git a/api/logic/minecraft/Component.h b/api/logic/minecraft/Component.h new file mode 100644 index 00000000..4917149b --- /dev/null +++ b/api/logic/minecraft/Component.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include +#include "meta/JsonFormat.h" +#include "ProblemProvider.h" +#include "QObjectPtr.h" +#include "multimc_logic_export.h" + +class ComponentList; +class LaunchProfile; +namespace Meta +{ + class Version; + class VersionList; +} +class VersionFile; + +class MULTIMC_LOGIC_EXPORT Component : public QObject, public ProblemProvider +{ +Q_OBJECT +public: + Component(ComponentList * parent, const QString &uid); + + // DEPRECATED: remove these constructors? + Component(ComponentList * parent, std::shared_ptr version); + Component(ComponentList * parent, const QString & uid, std::shared_ptr file); + + virtual ~Component(){}; + void applyTo(LaunchProfile *profile); + + bool isMoveable(); + bool isCustomizable(); + bool isRevertible(); + bool isRemovable(); + bool isCustom(); + bool isVersionChangeable(); + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code + void setOrder(int order); + int getOrder(); + + QString getID(); + QString getName(); + QString getVersion(); + std::shared_ptr getMeta(); + QDateTime getReleaseDateTime(); + + QString getFilename(); + + std::shared_ptr getVersionFile() const; + std::shared_ptr getVersionList() const; + + void setImportant (bool state); + + const QList getProblems() const override; + ProblemSeverity getProblemSeverity() const override; + + void setVersion(const QString & version); + bool customize(); + bool revert(); + + void updateCachedData(); + +signals: + void dataChanged(); + +public: /* data */ + ComponentList * m_parent; + + // BEGIN: persistent component list properties + /// ID of the component + QString m_uid; + /// version of the component - when there's a custom json override, this is also the version the component reverts to + QString m_version; + /// if true, this has been added automatically to satisfy dependencies and may be automatically removed + bool m_dependencyOnly = false; + /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed. + bool m_important = false; + + /// cached name for display purposes, taken from the version file (meta or local override) + QString m_cachedName; + /// cached version for display AND other purposes, taken from the version file (meta or local override) + QString m_cachedVersion; + /// cached set of requirements, taken from the version file (meta or local override) + Meta::RequireSet m_cachedRequires; + Meta::RequireSet m_cachedConflicts; + /// if true, the component is volatile and may be automatically removed when no longer needed + bool m_cachedVolatile = false; + // END: persistent component list properties + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code + bool m_orderOverride = false; + int m_order = 0; + + // load state + std::shared_ptr m_metaVersion; + std::shared_ptr m_file; + bool m_loaded = false; +}; + +typedef shared_qobject_ptr ComponentPtr; diff --git a/api/logic/minecraft/ComponentList.cpp b/api/logic/minecraft/ComponentList.cpp index 34d3bc14..342c10b3 100644 --- a/api/logic/minecraft/ComponentList.cpp +++ b/api/logic/minecraft/ComponentList.cpp @@ -21,7 +21,6 @@ #include #include -#include "minecraft/ComponentList.h" #include "Exception.h" #include #include @@ -30,67 +29,666 @@ #include #include #include +#include +#include + +#include "ComponentList.h" +#include "ComponentList_p.h" +#include "ComponentUpdateTask.h" ComponentList::ComponentList(MinecraftInstance * instance) : QAbstractListModel() { - m_instance = instance; + d.reset(new ComponentListData); + d->m_instance = instance; + d->m_saveTimer.setSingleShot(true); + d->m_saveTimer.setInterval(5000); + connect(&d->m_saveTimer, &QTimer::timeout, this, &ComponentList::save); } ComponentList::~ComponentList() { + if(saveIsScheduled()) + { + d->m_saveTimer.stop(); + save(); + } } -void ComponentList::reload() +// BEGIN: component file format + +static const int currentComponentsFileVersion = 1; + +static QJsonObject componentToJsonV1(ComponentPtr component) { - beginResetModel(); - load_internal(); - reapplyPatches(); - endResetModel(); + QJsonObject obj; + // critical + obj.insert("uid", component->m_uid); + if(!component->m_version.isEmpty()) + { + obj.insert("version", component->m_version); + } + if(component->m_dependencyOnly) + { + obj.insert("dependencyOnly", true); + } + if(component->m_important) + { + obj.insert("important", true); + } + + // cached + if(!component->m_cachedVersion.isEmpty()) + { + obj.insert("cachedVersion", component->m_cachedVersion); + } + if(!component->m_cachedName.isEmpty()) + { + obj.insert("cachedName", component->m_cachedName); + } + Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires"); + Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); + if(component->m_cachedVolatile) + { + obj.insert("cachedVolatile", true); + } + return obj; } -void ComponentList::clearPatches() +static ComponentPtr componentFromJsonV1(ComponentList * parent, const QString & componentJsonPattern, const QJsonObject &obj) { - beginResetModel(); - m_patches.clear(); - endResetModel(); + // critical + auto uid = Json::requireString(obj.value("uid")); + auto filePath = componentJsonPattern.arg(uid); + auto component = new Component(parent, uid); + component->m_version = Json::ensureString(obj.value("version")); + component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); + component->m_important = Json::ensureBoolean(obj.value("important"), false); + + // cached + // TODO @RESILIENCE: ignore invalid values/structure here? + component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion")); + component->m_cachedName = Json::ensureString(obj.value("cachedName")); + Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); + Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); + component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false); + return component; } -void ComponentList::appendPatch(ProfilePatchPtr patch) +// Save the given component container data to a file +static bool saveComponentList(const QString & filename, const ComponentContainer & container) { - int index = m_patches.size(); + QJsonObject obj; + obj.insert("formatVersion", currentComponentsFileVersion); + QJsonArray orderArray; + for(auto component: container) + { + orderArray.append(componentToJsonV1(component)); + } + obj.insert("components", orderArray); + QSaveFile outFile(filename); + if (!outFile.open(QFile::WriteOnly)) + { + qCritical() << "Couldn't open" << outFile.fileName() + << "for writing:" << outFile.errorString(); + return false; + } + auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); + if(outFile.write(data) != data.size()) + { + qCritical() << "Couldn't write all the data into" << outFile.fileName() + << "because:" << outFile.errorString(); + return false; + } + if(!outFile.commit()) + { + qCritical() << "Couldn't save" << outFile.fileName() + << "because:" << outFile.errorString(); + } + return true; +} + +// Read the given file into component containers +static bool loadComponentList(ComponentList * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container) +{ + QFile componentsFile(filename); + if (!componentsFile.exists()) + { + qWarning() << "Components file doesn't exist. This should never happen."; + return false; + } + if (!componentsFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << componentsFile.fileName() + << " for reading:" << componentsFile.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("formatVersion")); + if (version != currentComponentsFileVersion) + { + throw JSONValidationError(QObject::tr("Invalid component file version, expected %1") + .arg(currentComponentsFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("components")); + for(auto item: orderArray) + { + auto obj = Json::requireObject(item, "Component must be an object."); + container.append(componentFromJsonV1(parent, componentJsonPattern, obj)); + } + } + catch (JSONValidationError &err) + { + qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; + container.clear(); + return false; + } + return true; +} + +// END: component file format + +// BEGIN: save/load logic + +bool ComponentList::saveIsScheduled() const +{ + return d->dirty; +} + +void ComponentList::buildingFromScratch() +{ + d->loaded = true; + d->dirty = true; +} + +void ComponentList::scheduleSave() +{ + if(!d->loaded) + { + qDebug() << "Component list should never save if it didn't successfully load, instance:" << d->m_instance->name(); + return; + } + if(!d->dirty) + { + d->dirty = true; + qDebug() << "Component list save is scheduled for" << d->m_instance->name(); + } + d->m_saveTimer.start(); +} + +QString ComponentList::componentsFilePath() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); +} + +QString ComponentList::patchesPattern() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); +} + +QString ComponentList::patchFilePathForUid(const QString& uid) const +{ + return patchesPattern().arg(uid); +} + +void ComponentList::save() +{ + qDebug() << "Component list save performed now for" << d->m_instance->name(); + auto filename = componentsFilePath(); + saveComponentList(filename, d->components); + d->dirty = false; +} + +bool ComponentList::load() +{ + auto filename = componentsFilePath(); + QFile componentsFile(filename); + + // migrate old config to new one, if needed + if(!componentsFile.exists()) + { + if(!migratePreComponentConfig()) + { + // FIXME: the user should be notified... + qCritical() << "Failed to convert old pre-component config for instance" << d->m_instance->name(); + return false; + } + } + + // load the new component list and swap it with the current one... + ComponentContainer newComponents; + if(!loadComponentList(this, filename, patchesPattern(), newComponents)) + { + qCritical() << "Failed to load the component config for instance" << d->m_instance->name(); + return false; + } + else + { + // FIXME: actually use fine-grained updates, not this... + beginResetModel(); + // disconnect all the old components + for(auto component: d->components) + { + disconnect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + } + d->components.clear(); + d->componentIndex.clear(); + for(auto component: newComponents) + { + if(d->componentIndex.contains(component->m_uid)) + { + qWarning() << "Ignoring duplicate component entry" << component->m_uid; + continue; + } + connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + d->components.append(component); + d->componentIndex[component->m_uid] = component; + } + endResetModel(); + d->loaded = true; + return true; + } +} + +void ComponentList::reload(Net::Mode netmode) +{ + // Do not reload when the update/resolve task is running. It is in control. + if(d->m_updateTask) + { + return; + } + + // flush any scheduled saves to not lose state + if(saveIsScheduled()) + { + save(); + } + + // FIXME: differentiate when a reapply is required by propagating state from components + invalidateLaunchProfile(); + + if(load()) + { + resolve(netmode); + } +} + +shared_qobject_ptr ComponentList::getCurrentTask() +{ + return d->m_updateTask; +} + +void ComponentList::resolve(Net::Mode netmode) +{ + auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); + d->m_updateTask.reset(updateTask); + connect(updateTask, &ComponentUpdateTask::succeeded, this, &ComponentList::updateSucceeded); + connect(updateTask, &ComponentUpdateTask::failed, this, &ComponentList::updateFailed); + d->m_updateTask->start(); +} + + +void ComponentList::updateSucceeded() +{ + qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name(); + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +void ComponentList::updateFailed(const QString& error) +{ + qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error; + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +// NOTE this is really old stuff, and only needs to be used when loading the old hardcoded component-unaware format (loadPreComponentConfig). +static void upgradeDeprecatedFiles(QString root, QString instanceName) +{ + auto versionJsonPath = FS::PathCombine(root, "version.json"); + auto customJsonPath = FS::PathCombine(root, "custom.json"); + auto mcJson = FS::PathCombine(root, "patches" , "net.minecraft.json"); + + QString sourceFile; + QString renameFile; + + // convert old crap. + if(QFile::exists(customJsonPath)) + { + sourceFile = customJsonPath; + renameFile = versionJsonPath; + } + else if(QFile::exists(versionJsonPath)) + { + sourceFile = versionJsonPath; + } + if(!sourceFile.isEmpty() && !QFile::exists(mcJson)) + { + if(!FS::ensureFilePathExists(mcJson)) + { + qWarning() << "Couldn't create patches folder for" << instanceName; + return; + } + if(!renameFile.isEmpty() && QFile::exists(renameFile)) + { + if(!QFile::rename(renameFile, renameFile + ".old")) + { + qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << instanceName; + return; + } + } + auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false); + ProfileUtils::removeLwjglFromPatch(file); + file->uid = "net.minecraft"; + file->version = file->minecraftVersion; + file->name = "Minecraft"; + + Meta::Require needsLwjgl; + needsLwjgl.uid = "org.lwjgl"; + file->requires.insert(needsLwjgl); + + if(!ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), mcJson)) + { + return; + } + if(!QFile::rename(sourceFile, sourceFile + ".old")) + { + qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << instanceName; + return; + } + } +} + +/* + * Migrate old layout to the component based one... + * - Part of the version information is taken from `instance.cfg` (fed to this class from outside). + * - Part is taken from the old order.json file. + * - Part is loaded from loose json files in the instance's `patches` directory. + */ +bool ComponentList::migratePreComponentConfig() +{ + // upgrade the very old files from the beginnings of MultiMC 5 + upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name()); + + QList components; + QSet loaded; + + auto addBuiltinPatch = [&](const QString &uid, bool asDependency, const QString & emptyVersion, const Meta::Require & req, const Meta::Require & conflict) + { + auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(), "patches" , uid + ".json"); + auto intendedVersion = d->getOldConfigVersion(uid); + // load up the base minecraft patch + ComponentPtr component; + if(QFile::exists(jsonFilePath)) + { + if(intendedVersion.isEmpty()) + { + intendedVersion = emptyVersion; + } + auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false); + bool fileChanged = false; + // if uid is missing or incorrect, fix it + if(file->uid != uid) + { + file->uid = uid; + fileChanged = true; + } + // if version is missing, add it from the outside. + if(file->version.isEmpty()) + { + file->version = intendedVersion; + fileChanged = true; + } + // if this is a dependency (LWJGL), mark it also as volatile + if(asDependency) + { + file->m_volatile = true; + fileChanged = true; + } + // insert requirements if needed + if(!req.uid.isEmpty()) + { + file->requires.insert(req); + fileChanged = true; + } + // insert conflicts if needed + if(!conflict.uid.isEmpty()) + { + file->conflicts.insert(conflict); + fileChanged = true; + } + if(fileChanged) + { + // FIXME: @QUALITY do not ignore return value + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), jsonFilePath); + } + component = new Component(this, uid, file); + component->m_version = intendedVersion; + } + else if(!intendedVersion.isEmpty()) + { + auto metaVersion = ENV.metadataIndex()->get(uid, intendedVersion); + component = new Component(this, metaVersion); + } + else + { + return; + } + component->m_dependencyOnly = asDependency; + component->m_important = !asDependency; + components.append(component); + }; + // TODO: insert depends and conflicts here if these are customized files... + Meta::Require reqLwjgl; + reqLwjgl.uid = "org.lwjgl"; + reqLwjgl.suggests = "2.9.1"; + Meta::Require conflictLwjgl3; + conflictLwjgl3.uid = "org.lwjgl3"; + Meta::Require nullReq; + addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3); + addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq); + + // first, collect all other file-based patches and load them + QMap loadedComponents; + QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(),"patches")); + for (auto info : patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) + { + // parse the file + qDebug() << "Reading" << info.fileName(); + auto file = ProfileUtils::parseJsonFile(info, true); + + // correct missing or wrong uid based on the file name + QString uid = info.completeBaseName(); + + // ignore builtins, they've been handled already + if (uid == "net.minecraft") + continue; + if (uid == "org.lwjgl") + continue; + + // handle horrible corner cases + if(uid.isEmpty()) + { + // if you have a file named '.json', make it just go away. + // FIXME: @QUALITY do not ignore return value + QFile::remove(info.absoluteFilePath()); + continue; + } + bool fileChanged = false; + if(file->uid != uid) + { + file->uid = uid; + fileChanged = true; + } + if(fileChanged) + { + // FIXME: @QUALITY do not ignore return value + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), info.absoluteFilePath()); + } + + auto component = new Component(this, file->uid, file); + auto version = d->getOldConfigVersion(file->uid); + if(!version.isEmpty()) + { + component->m_version = version; + } + loadedComponents[file->uid] = component; + } + // try to load the other 'hardcoded' patches (forge, liteloader), if they weren't loaded from files + auto loadSpecial = [&](const QString & uid, int order) + { + auto patchVersion = d->getOldConfigVersion(uid); + if(!patchVersion.isEmpty() && !loadedComponents.contains(uid)) + { + auto patch = new Component(this, ENV.metadataIndex()->get(uid, patchVersion)); + patch->setOrder(order); + loadedComponents[uid] = patch; + } + }; + loadSpecial("net.minecraftforge", 5); + loadSpecial("com.mumfrey.liteloader", 10); + + // load the old order.json file, if present + ProfileUtils::PatchOrder userOrder; + ProfileUtils::readOverrideOrders(FS::PathCombine(d->m_instance->instanceRoot(), "order.json"), userOrder); + + // now add all the patches by user sort order + for (auto uid : userOrder) + { + // ignore builtins + if (uid == "net.minecraft") + continue; + if (uid == "org.lwjgl") + continue; + // ordering has a patch that is gone? + if(!loadedComponents.contains(uid)) + { + continue; + } + components.append(loadedComponents.take(uid)); + } + + // is there anything left to sort? - this is used when there are leftover components that aren't part of the order.json + if(!loadedComponents.isEmpty()) + { + // inserting into multimap by order number as key sorts the patches and detects duplicates + QMultiMap files; + auto iter = loadedComponents.begin(); + while(iter != loadedComponents.end()) + { + files.insert((*iter)->getOrder(), *iter); + iter++; + } + + // then just extract the patches and put them in the list + for (auto order : files.keys()) + { + const auto &values = files.values(order); + for(auto &value: values) + { + // TODO: put back the insertion of problem messages here, so the user knows about the id duplication + components.append(value); + } + } + } + // new we have a complete list of components... + return saveComponentList(componentsFilePath(), components); +} + +// END: save/load + +void ComponentList::appendComponent(ComponentPtr component) +{ + insertComponent(d->components.size(), component); +} + +void ComponentList::insertComponent(size_t index, ComponentPtr component) +{ + auto id = component->getID(); + if(id.isEmpty()) + { + qWarning() << "Attempt to add a component with empty ID!"; + return; + } + if(d->componentIndex.contains(id)) + { + qWarning() << "Attempt to add a component that is already present!"; + return; + } beginInsertRows(QModelIndex(), index, index); - m_patches.append(patch); + d->components.insert(index, component); + d->componentIndex[id] = component; endInsertRows(); + scheduleSave(); +} + +void ComponentList::componentDataChanged() +{ + auto objPtr = qobject_cast(sender()); + if(!objPtr) + { + qWarning() << "ComponentList got dataChenged signal from a non-Component!"; + return; + } + // figure out which one is it... in a seriously dumb way. + int index = 0; + for (auto component: d->components) + { + if(component.get() == objPtr) + { + emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + scheduleSave(); + return; + } + index++; + } + qWarning() << "ComponentList got dataChenged signal from a Component which does not belong to it!"; } bool ComponentList::remove(const int index) { - auto patch = versionPatch(index); + auto patch = getComponent(index); if (!patch->isRemovable()) { - qDebug() << "Patch" << patch->getID() << "is non-removable"; + qWarning() << "Patch" << patch->getID() << "is non-removable"; return false; } - if(!removePatch_internal(patch)) + if(!removeComponent_internal(patch)) { qCritical() << "Patch" << patch->getID() << "could not be removed"; return false; } beginRemoveRows(QModelIndex(), index, index); - m_patches.removeAt(index); + d->components.removeAt(index); + d->componentIndex.remove(patch->getID()); endRemoveRows(); - reapplyPatches(); - saveCurrentOrder(); + invalidateLaunchProfile(); + scheduleSave(); return true; } bool ComponentList::remove(const QString id) { int i = 0; - for (auto patch : m_patches) + for (auto patch : d->components) { if (patch->getID() == id) { @@ -103,66 +701,62 @@ bool ComponentList::remove(const QString id) bool ComponentList::customize(int index) { - auto patch = versionPatch(index); + auto patch = getComponent(index); if (!patch->isCustomizable()) { qDebug() << "Patch" << patch->getID() << "is not customizable"; return false; } - if(!customizePatch_internal(patch)) + if(!patch->customize()) { qCritical() << "Patch" << patch->getID() << "could not be customized"; return false; } - reapplyPatches(); - saveCurrentOrder(); - // FIXME: maybe later in unstable - // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + invalidateLaunchProfile(); + scheduleSave(); return true; } bool ComponentList::revertToBase(int index) { - auto patch = versionPatch(index); + auto patch = getComponent(index); if (!patch->isRevertible()) { qDebug() << "Patch" << patch->getID() << "is not revertible"; return false; } - if(!revertPatch_internal(patch)) + if(!patch->revert()) { qCritical() << "Patch" << patch->getID() << "could not be reverted"; return false; } - reapplyPatches(); - saveCurrentOrder(); - // FIXME: maybe later in unstable - // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + invalidateLaunchProfile(); + scheduleSave(); return true; } -ProfilePatchPtr ComponentList::versionPatch(const QString &id) +ComponentPtr ComponentList::getComponent(const QString &id) { - for (auto patch : m_patches) + auto iter = d->componentIndex.find(id); + if (iter == d->componentIndex.end()) { - if (patch->getID() == id) - { - return patch; - } + return nullptr; } - return nullptr; + return *iter; } -ProfilePatchPtr ComponentList::versionPatch(int index) +ComponentPtr ComponentList::getComponent(int index) { - if(index < 0 || index >= m_patches.size()) + if(index < 0 || index >= d->components.size()) + { return nullptr; - return m_patches[index]; + } + return d->components[index]; } bool ComponentList::isVanilla() { - for(auto patchptr: m_patches) + for(auto patchptr: d->components) { if(patchptr->isCustom()) return false; @@ -173,7 +767,7 @@ bool ComponentList::isVanilla() bool ComponentList::revertToVanilla() { // remove patches, if present - auto VersionPatchesCopy = m_patches; + auto VersionPatchesCopy = d->components; for(auto & it: VersionPatchesCopy) { if (!it->isCustom()) @@ -185,14 +779,14 @@ bool ComponentList::revertToVanilla() if(!remove(it->getID())) { qWarning() << "Couldn't remove" << it->getID() << "from profile!"; - reapplyPatches(); - saveCurrentOrder(); + invalidateLaunchProfile(); + scheduleSave(); return false; } } } - reapplyPatches(); - saveCurrentOrder(); + invalidateLaunchProfile(); + scheduleSave(); return true; } @@ -204,17 +798,17 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const int row = index.row(); int column = index.column(); - if (row < 0 || row >= m_patches.size()) + if (row < 0 || row >= d->components.size()) return QVariant(); - auto patch = m_patches.at(row); + auto patch = d->components.at(row); if (role == Qt::DisplayRole) { switch (column) { case 0: - return m_patches.at(row)->getName(); + return d->components.at(row)->getName(); case 1: { if(patch->isCustom()) @@ -283,7 +877,7 @@ Qt::ItemFlags ComponentList::flags(const QModelIndex &index) const int ComponentList::rowCount(const QModelIndex &parent) const { - return m_patches.size(); + return d->components.size(); } int ComponentList::columnCount(const QModelIndex &parent) const @@ -291,18 +885,6 @@ int ComponentList::columnCount(const QModelIndex &parent) const return 2; } -void ComponentList::saveCurrentOrder() const -{ - ProfileUtils::PatchOrder order; - for(auto item: m_patches) - { - if(!item->isMoveable()) - continue; - order.append(item->getID()); - } - saveOrder_internal(order); -} - void ComponentList::move(const int index, const MoveDirection direction) { int theirIndex; @@ -315,7 +897,7 @@ void ComponentList::move(const int index, const MoveDirection direction) theirIndex = index + 1; } - if (index < 0 || index >= m_patches.size()) + if (index < 0 || index >= d->components.size()) return; if (theirIndex >= rowCount()) theirIndex = rowCount() - 1; @@ -325,43 +907,23 @@ void ComponentList::move(const int index, const MoveDirection direction) return; int togap = theirIndex > index ? theirIndex + 1 : theirIndex; - auto from = versionPatch(index); - auto to = versionPatch(theirIndex); + auto from = getComponent(index); + auto to = getComponent(theirIndex); if (!from || !to || !to->isMoveable() || !from->isMoveable()) { return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); - m_patches.swap(index, theirIndex); + d->components.swap(index, theirIndex); endMoveRows(); - reapplyPatches(); - saveCurrentOrder(); -} -void ComponentList::resetOrder() -{ - resetOrder_internal(); - reload(); + invalidateLaunchProfile(); + scheduleSave(); } -bool ComponentList::reapplyPatches() +void ComponentList::invalidateLaunchProfile() { - try - { - m_profile.reset(new LaunchProfile); - for(auto file: m_patches) - { - qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); - file->applyTo(m_profile.get()); - } - } - catch (Exception & error) - { - m_profile.reset(); - qWarning() << "Couldn't apply profile patches because: " << error.cause(); - return false; - } - return true; + d->m_profile.reset(); } void ComponentList::installJarMods(QStringList selectedFiles) @@ -374,224 +936,7 @@ void ComponentList::installCustomJar(QString selectedFile) installCustomJar_internal(selectedFile); } - -/* - * TODO: get rid of this. Get rid of all order numbers. - */ -int ComponentList::getFreeOrderNumber() -{ - int largest = 100; - // yes, I do realize this is dumb. The order thing itself is dumb. and to be removed next. - for(auto thing: m_patches) - { - int order = thing->getOrder(); - if(order > largest) - largest = order; - } - return largest + 1; -} - -void ComponentList::upgradeDeprecatedFiles_internal() -{ - auto versionJsonPath = FS::PathCombine(m_instance->instanceRoot(), "version.json"); - auto customJsonPath = FS::PathCombine(m_instance->instanceRoot(), "custom.json"); - auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json"); - - QString sourceFile; - QString renameFile; - - // convert old crap. - if(QFile::exists(customJsonPath)) - { - sourceFile = customJsonPath; - renameFile = versionJsonPath; - } - else if(QFile::exists(versionJsonPath)) - { - sourceFile = versionJsonPath; - } - if(!sourceFile.isEmpty() && !QFile::exists(mcJson)) - { - if(!FS::ensureFilePathExists(mcJson)) - { - qWarning() << "Couldn't create patches folder for" << m_instance->name(); - return; - } - if(!renameFile.isEmpty() && QFile::exists(renameFile)) - { - if(!QFile::rename(renameFile, renameFile + ".old")) - { - qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << m_instance->name(); - return; - } - } - auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false); - ProfileUtils::removeLwjglFromPatch(file); - file->uid = "net.minecraft"; - file->version = file->minecraftVersion; - file->name = "Minecraft"; - auto data = OneSixVersionFormat::versionFileToJson(file, false).toJson(); - QSaveFile newPatchFile(mcJson); - if(!newPatchFile.open(QIODevice::WriteOnly)) - { - newPatchFile.cancelWriting(); - qWarning() << "Couldn't open main patch for writing in" << m_instance->name(); - return; - } - newPatchFile.write(data); - if(!newPatchFile.commit()) - { - qWarning() << "Couldn't save main patch in" << m_instance->name(); - return; - } - if(!QFile::rename(sourceFile, sourceFile + ".old")) - { - qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << m_instance->name(); - return; - } - } -} - -void ComponentList::loadDefaultBuiltinPatches_internal() -{ - auto addBuiltinPatch = [&](const QString &uid, const QString intendedVersion, int order) - { - auto jsonFilePath = FS::PathCombine(m_instance->instanceRoot(), "patches" , uid + ".json"); - // load up the base minecraft patch - ProfilePatchPtr profilePatch; - if(QFile::exists(jsonFilePath)) - { - auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false); - if(file->version.isEmpty()) - { - file->version = intendedVersion; - } - profilePatch = std::make_shared(file, jsonFilePath); - profilePatch->setVanilla(false); - profilePatch->setRevertible(true); - } - else - { - auto metaVersion = ENV.metadataIndex()->get(uid, intendedVersion); - profilePatch = std::make_shared(metaVersion); - profilePatch->setVanilla(true); - } - profilePatch->setOrder(order); - appendPatch(profilePatch); - }; - addBuiltinPatch("net.minecraft", m_instance->getComponentVersion("net.minecraft"), -2); - addBuiltinPatch("org.lwjgl", m_instance->getComponentVersion("org.lwjgl"), -1); -} - -void ComponentList::loadUserPatches_internal() -{ - // first, collect all patches (that are not builtins of OneSix) and load them - QMap loadedPatches; - QDir patchesDir(FS::PathCombine(m_instance->instanceRoot(),"patches")); - for (auto info : patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) - { - // parse the file - qDebug() << "Reading" << info.fileName(); - auto file = ProfileUtils::parseJsonFile(info, true); - // ignore builtins - if (file->uid == "net.minecraft") - continue; - if (file->uid == "org.lwjgl") - continue; - auto patch = std::make_shared(file, info.filePath()); - patch->setRemovable(true); - patch->setMovable(true); - if(ENV.metadataIndex()->hasUid(file->uid)) - { - // FIXME: requesting a uid/list creates it in the index... this allows reverting to possibly invalid versions... - patch->setRevertible(true); - } - loadedPatches[file->uid] = patch; - } - // these are 'special'... if not already loaded from instance files, grab them from the metadata repo. - auto loadSpecial = [&](const QString & uid, int order) - { - auto patchVersion = m_instance->getComponentVersion(uid); - if(!patchVersion.isEmpty() && !loadedPatches.contains(uid)) - { - auto patch = std::make_shared(ENV.metadataIndex()->get(uid, patchVersion)); - patch->setOrder(order); - patch->setVanilla(true); - patch->setRemovable(true); - patch->setMovable(true); - loadedPatches[uid] = patch; - } - }; - loadSpecial("net.minecraftforge", 5); - loadSpecial("com.mumfrey.liteloader", 10); - - // now add all the patches by user sort order - ProfileUtils::PatchOrder userOrder; - ProfileUtils::readOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), userOrder); - for (auto uid : userOrder) - { - // ignore builtins - if (uid == "net.minecraft") - continue; - if (uid == "org.lwjgl") - continue; - // ordering has a patch that is gone? - if(!loadedPatches.contains(uid)) - { - continue; - } - appendPatch(loadedPatches.take(uid)); - } - - // is there anything left to sort? - if(loadedPatches.isEmpty()) - { - // TODO: save the order here? - return; - } - - // inserting into multimap by order number as key sorts the patches and detects duplicates - QMultiMap files; - auto iter = loadedPatches.begin(); - while(iter != loadedPatches.end()) - { - files.insert((*iter)->getOrder(), *iter); - iter++; - } - - // then just extract the patches and put them in the list - for (auto order : files.keys()) - { - const auto &values = files.values(order); - for(auto &value: values) - { - // TODO: put back the insertion of problem messages here, so the user knows about the id duplication - appendPatch(value); - } - } - // TODO: save the order here? -} - - -void ComponentList::load_internal() -{ - clearPatches(); - upgradeDeprecatedFiles_internal(); - loadDefaultBuiltinPatches_internal(); - loadUserPatches_internal(); -} - -bool ComponentList::saveOrder_internal(ProfileUtils::PatchOrder order) const -{ - return ProfileUtils::writeOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), order); -} - -bool ComponentList::resetOrder_internal() -{ - return QDir(m_instance->instanceRoot()).remove("order.json"); -} - -bool ComponentList::removePatch_internal(ProfilePatchPtr patch) +bool ComponentList::removeComponent_internal(ComponentPtr patch) { bool ok = true; // first, remove the patch file. this ensures it's not used anymore @@ -605,10 +950,6 @@ bool ComponentList::removePatch_internal(ProfilePatchPtr patch) return false; } } - if(!m_instance->getComponentVersion(patch->getID()).isEmpty()) - { - m_instance->setComponentVersion(patch->getID(), QString()); - } // FIXME: we need a generic way of removing local resources, not just jar mods... auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool @@ -618,7 +959,7 @@ bool ComponentList::removePatch_internal(ProfilePatchPtr patch) return true; } QStringList jar, temp1, temp2, temp3; - jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, m_instance->jarmodsPath().absolutePath()); + jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath()); QFileInfo finfo (jar[0]); if(finfo.exists()) { @@ -633,90 +974,27 @@ bool ComponentList::removePatch_internal(ProfilePatchPtr patch) return true; }; - auto &jarMods = patch->getVersionFile()->jarMods; - for(auto &jarmod: jarMods) + auto vFile = patch->getVersionFile(); + if(vFile) { - ok &= preRemoveJarMod(jarmod); + auto &jarMods = vFile->jarMods; + for(auto &jarmod: jarMods) + { + ok &= preRemoveJarMod(jarmod); + } } return ok; } -bool ComponentList::customizePatch_internal(ProfilePatchPtr patch) -{ - if(patch->isCustom()) - { - return false; - } - - auto filename = FS::PathCombine(m_instance->instanceRoot(), "patches" , patch->getID() + ".json"); - if(!FS::ensureFilePathExists(filename)) - { - return false; - } - // FIXME: get rid of this try-catch. - try - { - QSaveFile jsonFile(filename); - if(!jsonFile.open(QIODevice::WriteOnly)) - { - return false; - } - auto vfile = patch->getVersionFile(); - if(!vfile) - { - return false; - } - auto document = OneSixVersionFormat::versionFileToJson(vfile, true); - jsonFile.write(document.toJson()); - if(!jsonFile.commit()) - { - return false; - } - load_internal(); - } - catch (Exception &error) - { - qWarning() << "Version could not be loaded:" << error.cause(); - } - return true; -} - -bool ComponentList::revertPatch_internal(ProfilePatchPtr patch) -{ - if(!patch->isCustom()) - { - // already not custom - return true; - } - auto filename = patch->getFilename(); - if(!QFile::exists(filename)) - { - // already gone / not custom - return true; - } - // just kill the file and reload - bool result = QFile::remove(filename); - // FIXME: get rid of this try-catch. - try - { - load_internal(); - } - catch (Exception &error) - { - qWarning() << "Version could not be loaded:" << error.cause(); - } - return result; -} - bool ComponentList::installJarMods_internal(QStringList filepaths) { - QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches"); + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) { return false; } - if (!FS::ensureFolderPathExists(m_instance->jarModsDir())) + if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) { return false; } @@ -729,7 +1007,7 @@ bool ComponentList::installJarMods_internal(QStringList filepaths) QString target_filename = id + ".jar"; QString target_id = "org.multimc.jarmod." + id; QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; - QString finalPath = FS::PathCombine(m_instance->jarModsDir(), target_filename); + QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); QFileInfo targetInfo(finalPath); if(targetInfo.exists()) @@ -751,7 +1029,6 @@ bool ComponentList::installJarMods_internal(QStringList filepaths) f->jarMods.append(jarMod); f->name = target_name; f->uid = target_id; - f->order = getFreeOrderNumber(); QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QFile file(patchFileName); @@ -761,28 +1038,25 @@ bool ComponentList::installJarMods_internal(QStringList filepaths) << "for reading:" << file.errorString(); return false; } - file.write(OneSixVersionFormat::versionFileToJson(f, true).toJson()); + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - auto patch = std::make_shared(f, patchFileName); - patch->setMovable(true); - patch->setRemovable(true); - appendPatch(patch); + appendComponent(new Component(this, f->uid, f)); } - saveCurrentOrder(); - reapplyPatches(); + scheduleSave(); + invalidateLaunchProfile(); return true; } bool ComponentList::installCustomJar_internal(QString filepath) { - QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches"); + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) { return false; } - QString libDir = m_instance->getLocalLibraryPath(); + QString libDir = d->m_instance->getLocalLibraryPath(); if (!FS::ensureFolderPathExists(libDir)) { return false; @@ -816,7 +1090,6 @@ bool ComponentList::installCustomJar_internal(QString filepath) f->mainJar = jarMod; f->name = target_name; f->uid = target_id; - f->order = getFreeOrderNumber(); QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QFile file(patchFileName); @@ -826,25 +1099,74 @@ bool ComponentList::installCustomJar_internal(QString filepath) << "for reading:" << file.errorString(); return false; } - file.write(OneSixVersionFormat::versionFileToJson(f, true).toJson()); + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - auto patch = std::make_shared(f, patchFileName); - patch->setMovable(true); - patch->setRemovable(true); - appendPatch(patch); + appendComponent(new Component(this, f->uid, f)); - saveCurrentOrder(); - reapplyPatches(); + scheduleSave(); + invalidateLaunchProfile(); return true; } std::shared_ptr ComponentList::getProfile() const { - return m_profile; + if(!d->m_profile) + { + try + { + auto profile = std::make_shared(); + for(auto file: d->components) + { + qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); + file->applyTo(profile.get()); + } + d->m_profile = profile; + } + catch (Exception & error) + { + qWarning() << "Couldn't apply profile patches because: " << error.cause(); + } + } + return d->m_profile; } -void ComponentList::clearProfile() +void ComponentList::setOldConfigVersion(const QString& uid, const QString& version) { - m_profile.reset(); + if(version.isEmpty()) + { + return; + } + d->m_oldConfigVersions[uid] = version; +} + +bool ComponentList::setComponentVersion(const QString& uid, const QString& version, bool important) +{ + auto iter = d->componentIndex.find(uid); + if(iter != d->componentIndex.end()) + { + // set existing + (*iter)->setVersion(version); + (*iter)->setImportant(important); + return true; + } + else + { + // add new + auto component = new Component(this, uid); + component->m_version = version; + component->m_important = important; + appendComponent(component); + return true; + } +} + +QString ComponentList::getComponentVersion(const QString& uid) const +{ + const auto iter = d->componentIndex.find(uid); + if (iter != d->componentIndex.end()) + { + return (*iter)->getVersion(); + } + return QString(); } diff --git a/api/logic/minecraft/ComponentList.h b/api/logic/minecraft/ComponentList.h index 0811a829..7971650d 100644 --- a/api/logic/minecraft/ComponentList.h +++ b/api/logic/minecraft/ComponentList.h @@ -23,19 +23,21 @@ #include "Library.h" #include "LaunchProfile.h" -#include "ProfilePatch.h" +#include "Component.h" #include "ProfileUtils.h" #include "BaseVersion.h" #include "MojangDownloadInfo.h" #include "multimc_logic_export.h" +#include "net/Mode.h" class MinecraftInstance; - +struct ComponentListData; +class ComponentUpdateTask; class MULTIMC_LOGIC_EXPORT ComponentList : public QAbstractListModel { Q_OBJECT - + friend ComponentUpdateTask; public: explicit ComponentList(MinecraftInstance * instance); virtual ~ComponentList(); @@ -46,6 +48,9 @@ public: virtual int columnCount(const QModelIndex &parent) const override; virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. + void buildingFromScratch(); + /// is this version unchanged by the user? bool isVanilla(); @@ -58,68 +63,76 @@ public: /// install a jar/zip as a replacement for the main jar void installCustomJar(QString selectedFile); - /// DEPRECATED, remove ASAP - int getFreeOrderNumber(); - enum MoveDirection { MoveUp, MoveDown }; - /// move patch file # up or down the list + /// move component file # up or down the list void move(const int index, const MoveDirection direction); - /// remove patch file # - including files/records + /// remove component file # - including files/records bool remove(const int index); - /// remove patch file by id - including files/records + /// remove component file by id - including files/records bool remove(const QString id); bool customize(int index); bool revertToBase(int index); - void resetOrder(); + /// reload the list, reload all components, resolve dependencies + void reload(Net::Mode netmode); - /// reload all profile patches from storage, clear the profile and apply the patches - void reload(); + // reload all components, resolve dependencies + void resolve(Net::Mode netmode); - /// apply the patches. Catches all the errors and returns true/false for success/failure - bool reapplyPatches(); + /// get current running task... + shared_qobject_ptr getCurrentTask(); std::shared_ptr getProfile() const; - void clearProfile(); + + // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config + void setOldConfigVersion(const QString &uid, const QString &version); + + QString getComponentVersion(const QString &uid) const; + + bool setComponentVersion(const QString &uid, const QString &version, bool important = false); + + QString patchFilePathForUid(const QString &uid) const; public: - /// get the profile patch by id - ProfilePatchPtr versionPatch(const QString &id); + /// get the profile component by id + ComponentPtr getComponent(const QString &id); - /// get the profile patch by index - ProfilePatchPtr versionPatch(int index); - - /// save the current patch order - void saveCurrentOrder() const; - - /// Remove all the patches - void clearPatches(); - - /// Add the patch object to the internal list of patches - void appendPatch(ProfilePatchPtr patch); + /// get the profile component by index + ComponentPtr getComponent(int index); private: - void load_internal(); - bool resetOrder_internal(); - bool saveOrder_internal(ProfileUtils::PatchOrder order) const; + void scheduleSave(); + bool saveIsScheduled() const; + + /// apply the component patches. Catches all the errors and returns true/false for success/failure + void invalidateLaunchProfile(); + + /// Add the component to the internal list of patches + void appendComponent(ComponentPtr component); + /// insert component so that its index is ideally the specified one (returns real index) + void insertComponent(size_t index, ComponentPtr component); + + QString componentsFilePath() const; + QString patchesPattern() const; + +private slots: + void save(); + void updateSucceeded(); + void updateFailed(const QString & error); + void componentDataChanged(); + +private: + bool load(); bool installJarMods_internal(QStringList filepaths); bool installCustomJar_internal(QString filepath); - bool removePatch_internal(ProfilePatchPtr patch); - bool customizePatch_internal(ProfilePatchPtr patch); - bool revertPatch_internal(ProfilePatchPtr patch); - void loadDefaultBuiltinPatches_internal(); - void loadUserPatches_internal(); - void upgradeDeprecatedFiles_internal(); + bool removeComponent_internal(ComponentPtr patch); + + bool migratePreComponentConfig(); private: /* data */ - /// list of attached profile patches - QList m_patches; - // the instance this belongs to - MinecraftInstance *m_instance; - - std::shared_ptr m_profile; + std::unique_ptr d; }; diff --git a/api/logic/minecraft/ComponentList_p.h b/api/logic/minecraft/ComponentList_p.h new file mode 100644 index 00000000..26ca5049 --- /dev/null +++ b/api/logic/minecraft/ComponentList_p.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Component.h" +#include +#include +#include +#include + +class MinecraftInstance; +using ComponentContainer = QList; +using ComponentIndex = QMap; +using ConnectionList = QList; + +struct ComponentListData +{ + // the instance this belongs to + MinecraftInstance *m_instance; + + // the launch profile (volatile, temporary thing created on demand) + std::shared_ptr m_profile; + + // version information migrated from instance.cfg file. Single use on migration! + std::map m_oldConfigVersions; + QString getOldConfigVersion(const QString& uid) const + { + const auto iter = m_oldConfigVersions.find(uid); + if(iter != m_oldConfigVersions.cend()) + { + return (*iter).second; + } + return QString(); + } + + // persistent list of components and related machinery + ComponentContainer components; + ComponentIndex componentIndex; + bool dirty = false; + QTimer m_saveTimer; + shared_qobject_ptr m_updateTask; + bool loaded = false; +}; + diff --git a/api/logic/minecraft/ComponentUpdateTask.cpp b/api/logic/minecraft/ComponentUpdateTask.cpp new file mode 100644 index 00000000..e03318c5 --- /dev/null +++ b/api/logic/minecraft/ComponentUpdateTask.cpp @@ -0,0 +1,688 @@ +#include "ComponentUpdateTask.h" + +#include "ComponentList_p.h" +#include "ComponentList.h" +#include "Component.h" +#include +#include +#include +#include +#include "ComponentUpdateTask_p.h" +#include +#include +#include "net/Mode.h" +#include "OneSixVersionFormat.h" + +/* + * This is responsible for loading the components of a component list AND resolving dependency issues between them + */ + +/* + * FIXME: the 'one shot async task' nature of this does not fit the intended usage + * Really, it should be a reactor/state machine that receives input from the application + * and dynamically adapts to changing requirements... + * + * The reactor should be the only entry into manipulating the ComponentList. + * See: https://en.wikipedia.org/wiki/Reactor_pattern + */ + +/* + * Or make this operate on a snapshot of the ComponentList state, then merge results in as long as the snapshot and ComponentList didn't change? + * If the component list changes, start over. + */ + +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent) + : Task(parent) +{ + d.reset(new ComponentUpdateTaskData); + d->m_list = list; + d->mode = mode; + d->netmode = netmode; +} + +ComponentUpdateTask::~ComponentUpdateTask() +{ +} + +void ComponentUpdateTask::executeTask() +{ + qDebug() << "Loading components"; + loadComponents(); +} + +namespace +{ +enum class LoadResult +{ + LoadedLocal, + RequiresRemote, + Failed +}; + +LoadResult composeLoadResult(LoadResult a, LoadResult b) +{ + if (a < b) + { + return b; + } + return a; +} + +static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr& loadTask, Net::Mode netmode) +{ + if(component->m_loaded) + { + qDebug() << component->getName() << "is already loaded"; + return LoadResult::LoadedLocal; + } + + LoadResult result = LoadResult::Failed; + auto customPatchFilename = component->getFilename(); + if(QFile::exists(customPatchFilename)) + { + // if local file exists... + + // check for uid problems inside... + bool fileChanged = false; + auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); + if(file->uid != component->m_uid) + { + file->uid = component->m_uid; + fileChanged = true; + } + if(fileChanged) + { + // FIXME: @QUALITY do not ignore return value + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); + } + + component->m_file = file; + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + auto metaVersion = ENV.metadataIndex()->get(component->m_uid, component->m_version); + component->m_metaVersion = metaVersion; + if(metaVersion->isLoaded()) + { + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + metaVersion->load(netmode); + loadTask = metaVersion->getCurrentTask(); + if(loadTask) + result = LoadResult::RequiresRemote; + else if (metaVersion->isLoaded()) + result = LoadResult::LoadedLocal; + else + result = LoadResult::Failed; + } + } + return result; +} + +static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr& loadTask, Net::Mode netmode) +{ + if(component->m_loaded) + { + qDebug() << component->getName() << "is already loaded"; + return LoadResult::LoadedLocal; + } + + LoadResult result = LoadResult::Failed; + auto metaList = ENV.metadataIndex()->get(component->m_uid); + if(metaList->isLoaded()) + { + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + metaList->load(netmode); + loadTask = metaList->getCurrentTask(); + result = LoadResult::RequiresRemote; + } + return result; +} + +static LoadResult loadIndex(shared_qobject_ptr& loadTask, Net::Mode netmode) +{ + // FIXME: DECIDE. do we want to run the update task anyway? + if(ENV.metadataIndex()->isLoaded()) + { + qDebug() << "Index is already loaded"; + return LoadResult::LoadedLocal; + } + ENV.metadataIndex()->load(netmode); + loadTask = ENV.metadataIndex()->getCurrentTask(); + if(loadTask) + { + return LoadResult::RequiresRemote; + } + // FIXME: this is assuming the load succeeded... did it really? + return LoadResult::LoadedLocal; +} +} + +void ComponentUpdateTask::loadComponents() +{ + LoadResult result = LoadResult::LoadedLocal; + size_t taskIndex = 0; + size_t componentIndex = 0; + d->remoteLoadSuccessful = true; + // load the main index (it is needed to determine if components can revert) + { + // FIXME: tear out as a method? or lambda? + shared_qobject_ptr indexLoadTask; + auto singleResult = loadIndex(indexLoadTask, d->netmode); + result = composeLoadResult(result, singleResult); + if(indexLoadTask) + { + qDebug() << "Remote loading is being run for metadata index"; + RemoteLoadStatus status; + status.type = RemoteLoadStatus::Type::Index; + d->remoteLoadStatusList.append(status); + connect(indexLoadTask.get(), &Task::succeeded, [=]() + { + remoteLoadSucceeded(taskIndex); + }); + connect(indexLoadTask.get(), &Task::failed, [=](const QString & error) + { + remoteLoadFailed(taskIndex, error); + }); + taskIndex++; + } + } + // load all the components OR their lists... + for (auto component: d->m_list->d->components) + { + shared_qobject_ptr loadTask; + LoadResult singleResult; + RemoteLoadStatus::Type loadType; + // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, ignore all that... +#if 0 + switch(d->mode) + { + case Mode::Launch: + { + singleResult = loadComponent(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::Version; + break; + } + case Mode::Resolution: + { + singleResult = loadComponentList(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::List; + break; + } + } +#else + singleResult = loadComponent(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::Version; +#endif + if(singleResult == LoadResult::LoadedLocal) + { + component->updateCachedData(); + } + result = composeLoadResult(result, singleResult); + if (loadTask) + { + qDebug() << "Remote loading is being run for" << component->getName(); + connect(loadTask.get(), &Task::succeeded, [=]() + { + remoteLoadSucceeded(taskIndex); + }); + connect(loadTask.get(), &Task::failed, [=](const QString & error) + { + remoteLoadFailed(taskIndex, error); + }); + RemoteLoadStatus status; + status.type = loadType; + status.componentListIndex = componentIndex; + d->remoteLoadStatusList.append(status); + taskIndex++; + } + componentIndex++; + } + d->remoteTasksInProgress = taskIndex; + switch(result) + { + case LoadResult::LoadedLocal: + { + // Everything got loaded. Advance to dependency resolution. + resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); + break; + } + case LoadResult::RequiresRemote: + { + // we wait for signals. + break; + } + case LoadResult::Failed: + { + emitFailed(tr("Some component metadata load tasks failed.")); + break; + } + } +} + +namespace +{ + struct RequireEx : public Meta::Require + { + size_t indexOfFirstDependee = 0; + }; + struct RequireCompositionResult + { + bool ok; + RequireEx outcome; + }; + using RequireExSet = std::set; +} + +static RequireCompositionResult composeRequirement(const RequireEx & a, const RequireEx & b) +{ + assert(a.uid == b.uid); + RequireEx out; + out.uid = a.uid; + out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); + if(a.equalsVersion.isEmpty()) + { + out.equalsVersion = b.equalsVersion; + } + else if (b.equalsVersion.isEmpty()) + { + out.equalsVersion = a.equalsVersion; + } + else if (a.equalsVersion == b.equalsVersion) + { + out.equalsVersion = a.equalsVersion; + } + else + { + // FIXME: mark error as explicit version conflict + return {false, out}; + } + + if(a.suggests.isEmpty()) + { + out.suggests = b.suggests; + } + else if (b.suggests.isEmpty()) + { + out.suggests = a.suggests; + } + else + { + Version aVer(a.suggests); + Version bVer(b.suggests); + out.suggests = (aVer < bVer ? b.suggests : a.suggests); + } + return {true, out}; +} + +// gather the requirements from all components, finding any obvious conflicts +static bool gatherRequirementsFromComponents(const ComponentContainer & input, RequireExSet & output) +{ + bool succeeded = true; + size_t componentNum = 0; + for(auto component: input) + { + auto &componentRequires = component->m_cachedRequires; + for(const auto & componentRequire: componentRequires) + { + auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require & req){ + return req.uid == componentRequire.uid; + }); + + RequireEx componenRequireEx; + componenRequireEx.uid = componentRequire.uid; + componenRequireEx.suggests = componentRequire.suggests; + componenRequireEx.equalsVersion = componentRequire.equalsVersion; + componenRequireEx.indexOfFirstDependee = componentNum; + + if(found != output.cend()) + { + // found... process it further + auto result = composeRequirement(componenRequireEx, *found); + if(result.ok) + { + output.erase(componenRequireEx); + output.insert(result.outcome); + } + else + { + qCritical() + << "Conflicting requirements:" + << componentRequire.uid + << "versions:" + << componentRequire.equalsVersion + << ";" + << (*found).equalsVersion; + } + succeeded &= result.ok; + } + else + { + // not found, accumulate + output.insert(componenRequireEx); + } + } + componentNum++; + } + return succeeded; +} + +/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) +static void getTrivialRemovals(const ComponentContainer & components, const RequireExSet & reqs, QStringList &toRemove) +{ + for(const auto & component: components) + { + if(!component->m_dependencyOnly) + continue; + if(!component->m_cachedVolatile) + continue; + RequireEx reqNeedle; + reqNeedle.uid = component->m_uid; + const auto iter = reqs.find(reqNeedle); + if(iter == reqs.cend()) + { + toRemove.append(component->m_uid); + } + } +} + +/** + * handles: + * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) + * - trivial version conflict of dependencies == explicit version required and installed is different + * + * toAdd - set of requirements than mean adding a new component + * toChange - set of requirements that mean changing version of an existing component + */ +static bool getTrivialComponentChanges(const ComponentIndex & index, const RequireExSet & input, RequireExSet & toAdd, RequireExSet & toChange) +{ + enum class Decision + { + Undetermined, + Met, + Missing, + VersionNotSame, + LockedVersionNotSame + } decision = Decision::Undetermined; + + QString reqStr; + bool succeeded = true; + // list the composed requirements and say if they are met or unmet + for(auto & req: input) + { + do + { + if(req.equalsVersion.isEmpty()) + { + reqStr = QString("Req: %1").arg(req.uid); + if(index.contains(req.uid)) + { + decision = Decision::Met; + } + else + { + toAdd.insert(req); + decision = Decision::Missing; + } + break; + } + else + { + reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); + const auto & compIter = index.find(req.uid); + if(compIter == index.cend()) + { + toAdd.insert(req); + decision = Decision::Missing; + break; + } + auto & comp = (*compIter); + if(comp->getVersion() != req.equalsVersion) + { + if(comp->m_dependencyOnly) + { + decision = Decision::VersionNotSame; + } + else + { + decision = Decision::LockedVersionNotSame; + } + break; + } + decision = Decision::Met; + } + } while(false); + switch(decision) + { + case Decision::Undetermined: + qCritical() << "No decision for" << reqStr; + succeeded = false; + break; + case Decision::Met: + qDebug() << reqStr << "Is met."; + break; + case Decision::Missing: + qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; + toAdd.insert(req); + break; + case Decision::VersionNotSame: + qDebug() << reqStr << "already has different version that can be changed."; + toChange.insert(req); + break; + case Decision::LockedVersionNotSame: + qDebug() << reqStr << "already has different version that cannot be changed."; + succeeded = false; + break; + } + } + return succeeded; +} + +// FIXME, TODO: decouple dependency resolution from loading +// FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses. +// FIXME: throw all this away and use a graph +void ComponentUpdateTask::resolveDependencies(bool checkOnly) +{ + qDebug() << "Resolving dependencies"; + /* + * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways: + * 1. There are conflicting dependencies on the same uid with different exact version numbers + * -> hard error + * 2. A dependency has non-matching exact version number + * -> hard error + * 3. A dependency is entirely missing and needs to be injected before the dependee(s) + * -> requirements are injected + * + * NOTE: this is a placeholder and should eventually be replaced with something 'serious' + */ + auto & components = d->m_list->d->components; + auto & componentIndex = d->m_list->d->componentIndex; + + RequireExSet allRequires; + QStringList toRemove; + do + { + allRequires.clear(); + toRemove.clear(); + if(!gatherRequirementsFromComponents(components, allRequires)) + { + emitFailed(tr("Conflicting requirements detected during dependency checking!")); + return; + } + getTrivialRemovals(components, allRequires, toRemove); + if(!toRemove.isEmpty()) + { + qDebug() << "Removing obsolete components..."; + for(auto & remove : toRemove) + { + qDebug() << "Removing" << remove; + d->m_list->remove(remove); + } + } + } while (!toRemove.isEmpty()); + RequireExSet toAdd; + RequireExSet toChange; + bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); + if(!succeeded) + { + emitFailed(tr("Instance has conflicting dependencies.")); + return; + } + if(checkOnly) + { + if(toAdd.size() || toChange.size()) + { + emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); + } + else + { + emitSucceeded(); + } + return; + } + + bool recursionNeeded = false; + if(toAdd.size()) + { + // add stuff... + for(auto &add: toAdd) + { + ComponentPtr component = new Component(d->m_list, add.uid); + if(!add.equalsVersion.isEmpty()) + { + // exact version + qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; + component->m_version = add.equalsVersion; + } + else + { + // version needs to be decided + qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; +// ############################################################################################################ +// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. + if(!add.suggests.isEmpty()) + { + component->m_version = add.suggests; + } + else + { + if(add.uid == "org.lwjgl") + { + component->m_version = "2.9.1"; + } + else if (add.uid == "org.lwjgl3") + { + component->m_version = "3.1.2"; + } + } +// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. +// ############################################################################################################ + } + component->m_dependencyOnly = true; + // FIXME: this should not work directly with the component list + d->m_list->insertComponent(add.indexOfFirstDependee, component); + componentIndex[add.uid] = component; + } + recursionNeeded = true; + } + if(toChange.size()) + { + // change a version of something that exists + for(auto &change: toChange) + { + // FIXME: this should not work directly with the component list + qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion; + auto component = componentIndex[change.uid]; + component->setVersion(change.equalsVersion); + } + recursionNeeded = true; + } + + if(recursionNeeded) + { + loadComponents(); + } + else + { + emitSucceeded(); + } +} + +void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) +{ + auto &taskSlot = d->remoteLoadStatusList[taskIndex]; + if(taskSlot.finished) + { + qWarning() << "Got multiple results from remote load task" << taskIndex; + return; + } + qDebug() << "Remote task" << taskIndex << "succeeded"; + taskSlot.succeeded = false; + taskSlot.finished = true; + d->remoteTasksInProgress --; + // update the cached data of the component from the downloaded version file. + if (taskSlot.type == RemoteLoadStatus::Type::Version) + { + auto component = d->m_list->getComponent(taskSlot.componentListIndex); + component->m_loaded = true; + component->updateCachedData(); + } + checkIfAllFinished(); +} + + +void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) +{ + auto &taskSlot = d->remoteLoadStatusList[taskIndex]; + if(taskSlot.finished) + { + qWarning() << "Got multiple results from remote load task" << taskIndex; + return; + } + qDebug() << "Remote task" << taskIndex << "failed: " << msg; + d->remoteLoadSuccessful = false; + taskSlot.succeeded = false; + taskSlot.finished = true; + taskSlot.error = msg; + d->remoteTasksInProgress --; + checkIfAllFinished(); +} + +void ComponentUpdateTask::checkIfAllFinished() +{ + if(d->remoteTasksInProgress) + { + // not yet... + return; + } + if(d->remoteLoadSuccessful) + { + // nothing bad happened... clear the temp load status and proceed with looking at dependencies + d->remoteLoadStatusList.clear(); + resolveDependencies(d->mode == Mode::Launch); + } + else + { + // remote load failed... report error and bail + QStringList allErrorsList; + for(auto & item: d->remoteLoadStatusList) + { + if(!item.succeeded) + { + allErrorsList.append(item.error); + } + } + auto allErrors = allErrorsList.join("\n"); + emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); + d->remoteLoadStatusList.clear(); + } +} diff --git a/api/logic/minecraft/ComponentUpdateTask.h b/api/logic/minecraft/ComponentUpdateTask.h new file mode 100644 index 00000000..11d122b6 --- /dev/null +++ b/api/logic/minecraft/ComponentUpdateTask.h @@ -0,0 +1,37 @@ +#pragma once + +#include "tasks/Task.h" +#include "net/Mode.h" + +#include +class ComponentList; +struct ComponentUpdateTaskData; + +class ComponentUpdateTask : public Task +{ + Q_OBJECT +public: + enum class Mode + { + Launch, + Resolution + }; + +public: + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList * list, QObject *parent = 0); + virtual ~ComponentUpdateTask(); + +protected: + void executeTask(); + +private: + void loadComponents(); + void resolveDependencies(bool checkOnly); + + void remoteLoadSucceeded(size_t index); + void remoteLoadFailed(size_t index, const QString &msg); + void checkIfAllFinished(); + +private: + std::unique_ptr d; +}; diff --git a/api/logic/minecraft/ComponentUpdateTask_p.h b/api/logic/minecraft/ComponentUpdateTask_p.h new file mode 100644 index 00000000..a5216506 --- /dev/null +++ b/api/logic/minecraft/ComponentUpdateTask_p.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include "net/Mode.h" + +class ComponentList; + +struct RemoteLoadStatus +{ + enum class Type + { + Index, + List, + Version + } type = Type::Version; + size_t componentListIndex = 0; + bool finished = false; + bool succeeded = false; + QString error; +}; + +struct ComponentUpdateTaskData +{ + ComponentList * m_list = nullptr; + QList remoteLoadStatusList; + bool remoteLoadSuccessful = true; + size_t remoteTasksInProgress = 0; + ComponentUpdateTask::Mode mode; + Net::Mode netmode; +}; diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 76e70e5b..b7deed77 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -34,6 +34,7 @@ #include "ComponentList.h" #include "AssetsUtils.h" #include "MinecraftUpdate.h" +#include "MinecraftLoadAndCheck.h" #define IBUS "@im=ibus" @@ -63,12 +64,6 @@ private: MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) : BaseInstance(globalSettings, settings, rootDir) { - // FIXME: remove these - m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); - m_settings->registerSetting("LWJGLVersion", "2.9.1"); - m_settings->registerSetting("ForgeVersion", ""); - m_settings->registerSetting("LiteloaderVersion", ""); - // Java Settings auto javaOverride = m_settings->registerSetting("OverrideJava", false); auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); @@ -101,11 +96,23 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO // Minecraft launch method auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false); m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride); + + // DEPRECATED: Read what versions the user configuration thinks should be used + m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); + m_settings->registerSetting("LWJGLVersion", ""); + m_settings->registerSetting("ForgeVersion", ""); + m_settings->registerSetting("LiteloaderVersion", ""); + + m_components.reset(new ComponentList(this)); + m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString()); + auto setting = m_settings->getSetting("LWJGLVersion"); + m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString()); + m_components->setOldConfigVersion("net.minecraftforge", m_settings->get("ForgeVersion").toString()); + m_components->setOldConfigVersion("com.mumfrey.liteloader", m_settings->get("LiteloaderVersion").toString()); } void MinecraftInstance::init() { - createProfile(); } QString MinecraftInstance::typeName() const @@ -113,41 +120,6 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } - -bool MinecraftInstance::reload() -{ - if (BaseInstance::reload()) - { - try - { - reloadProfile(); - return true; - } - catch (...) - { - return false; - } - } - return false; -} - -void MinecraftInstance::createProfile() -{ - m_components.reset(new ComponentList(this)); -} - -void MinecraftInstance::reloadProfile() -{ - m_components->reload(); - emit versionReloaded(); -} - -void MinecraftInstance::clearProfile() -{ - m_components->clearProfile(); - emit versionReloaded(); -} - std::shared_ptr MinecraftInstance::getComponentList() const { return m_components; @@ -771,7 +743,7 @@ QString MinecraftInstance::getStatusbarDescription() } QString description; - description.append(tr("Minecraft %1 (%2)").arg(getComponentVersion("net.minecraft")).arg(typeName())); + description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName())); if(totalTimePlayed() > 0) { description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); @@ -783,9 +755,20 @@ QString MinecraftInstance::getStatusbarDescription() return description; } -shared_qobject_ptr MinecraftInstance::createUpdateTask() +shared_qobject_ptr MinecraftInstance::createUpdateTask(Net::Mode mode) { - return shared_qobject_ptr(new OneSixUpdate(this)); + switch (mode) + { + case Net::Mode::Offline: + { + return shared_qobject_ptr(new MinecraftLoadAndCheck(this)); + } + case Net::Mode::Online: + { + return shared_qobject_ptr(new OneSixUpdate(this)); + } + } + return nullptr; } std::shared_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session) @@ -827,11 +810,14 @@ std::shared_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr s if(session->status != AuthSession::PlayableOffline) { process->appendStep(std::make_shared(pptr, session)); - process->appendStep(std::make_shared(pptr)); + process->appendStep(std::make_shared(pptr, Net::Mode::Online)); + } + else + { + process->appendStep(std::make_shared(pptr, Net::Mode::Offline)); } // if there are any jar mods - if(getJarMods().size()) { auto step = std::make_shared(pptr); process->appendStep(step); @@ -900,53 +886,6 @@ JavaVersion MinecraftInstance::getJavaVersion() const return JavaVersion(settings()->get("JavaVersion").toString()); } -bool MinecraftInstance::setComponentVersion(const QString& uid, const QString& version) -{ - if(uid == "net.minecraft") - { - settings()->set("IntendedVersion", version); - } - else if (uid == "org.lwjgl") - { - settings()->set("LWJGLVersion", version); - } - else if (uid == "net.minecraftforge") - { - settings()->set("ForgeVersion", version); - } - else if (uid == "com.mumfrey.liteloader") - { - settings()->set("LiteloaderVersion", version); - } - if(getComponentList()) - { - clearProfile(); - } - emit propertiesChanged(this); - return true; -} - -QString MinecraftInstance::getComponentVersion(const QString& uid) const -{ - if(uid == "net.minecraft") - { - return settings()->get("IntendedVersion").toString(); - } - else if(uid == "org.lwjgl") - { - return settings()->get("LWJGLVersion").toString(); - } - else if(uid == "net.minecraftforge") - { - return settings()->get("ForgeVersion").toString(); - } - else if(uid == "com.mumfrey.liteloader") - { - return settings()->get("LiteloaderVersion").toString(); - } - return QString(); -} - std::shared_ptr MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index cea7c60a..13a3753e 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -53,12 +53,7 @@ public: ////// Profile management ////// - void createProfile(); std::shared_ptr getComponentList() const; - void reloadProfile(); - void clearProfile(); - bool reload() override; - ////// Mod Lists ////// std::shared_ptr loaderModList() const; @@ -69,7 +64,7 @@ public: ////// Launch stuff ////// - shared_qobject_ptr createUpdateTask() override; + shared_qobject_ptr createUpdateTask(Net::Mode mode) override; std::shared_ptr createLaunchTask(AuthSessionPtr account) override; QStringList extraArguments() const override; QStringList verboseDescription(AuthSessionPtr session) override; @@ -105,11 +100,6 @@ public: virtual JavaVersion getJavaVersion() const; - // FIXME: remove - QString getComponentVersion(const QString &uid) const; - // FIXME: remove - bool setComponentVersion(const QString &uid, const QString &version); - signals: void versionReloaded(); diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.cpp b/api/logic/minecraft/MinecraftLoadAndCheck.cpp new file mode 100644 index 00000000..c64bbddf --- /dev/null +++ b/api/logic/minecraft/MinecraftLoadAndCheck.cpp @@ -0,0 +1,45 @@ +#include "MinecraftLoadAndCheck.h" +#include "MinecraftInstance.h" +#include "ComponentList.h" + +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void MinecraftLoadAndCheck::executeTask() +{ + // add offline metadata load task + auto components = m_inst->getComponentList(); + components->reload(Net::Mode::Offline); + m_task = components->getCurrentTask(); + + if(!m_task) + { + emitSucceeded(); + return; + } + connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::subtaskSucceeded); + connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); + connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); + connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); +} + +void MinecraftLoadAndCheck::subtaskSucceeded() +{ + if(isFinished()) + { + qCritical() << "OneSixUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + return; + } + emitSucceeded(); +} + +void MinecraftLoadAndCheck::subtaskFailed(QString error) +{ + if(isFinished()) + { + qCritical() << "OneSixUpdate: Subtask" << sender() << "failed, but work was already done!"; + return; + } + emitFailed(error); +} diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.h b/api/logic/minecraft/MinecraftLoadAndCheck.h new file mode 100644 index 00000000..f41c3d2f --- /dev/null +++ b/api/logic/minecraft/MinecraftLoadAndCheck.h @@ -0,0 +1,47 @@ +/* Copyright 2013-2017 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "tasks/Task.h" +#include + +#include "QObjectPtr.h" + +class MinecraftVersion; +class MinecraftInstance; + +class MinecraftLoadAndCheck : public Task +{ + Q_OBJECT +public: + explicit MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent = 0); + void executeTask() override; + +private slots: + void subtaskSucceeded(); + void subtaskFailed(QString error); + +private: + MinecraftInstance *m_inst = nullptr; + shared_qobject_ptr m_task; + QString m_preFailure; + QString m_fail_reason; +}; + diff --git a/api/logic/minecraft/MinecraftUpdate.cpp b/api/logic/minecraft/MinecraftUpdate.cpp index 529cf13e..12b11544 100644 --- a/api/logic/minecraft/MinecraftUpdate.cpp +++ b/api/logic/minecraft/MinecraftUpdate.cpp @@ -39,40 +39,24 @@ OneSixUpdate::OneSixUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { +} + +void OneSixUpdate::executeTask() +{ + m_tasks.clear(); // create folders { m_tasks.append(std::make_shared(m_inst)); } - // add metadata update tasks, if necessary + // add metadata update task if necessary { - /* - * FIXME: there are some corner cases here that remain unhandled: - * what if local load succeeds but remote fails? The version is still usable... - * We should not rely on the remote to be there... and prefer local files if it does not respond. - */ - qDebug() << "Updating patches..."; - auto profile = m_inst->getComponentList(); - m_inst->reloadProfile(); - for(int i = 0; i < profile->rowCount(); i++) + auto components = m_inst->getComponentList(); + components->reload(Net::Mode::Online); + auto task = components->getCurrentTask(); + if(task) { - auto patch = profile->versionPatch(i); - auto id = patch->getID(); - auto metadata = patch->getMeta(); - if(metadata) - { - metadata->load(); - auto task = metadata->getCurrentTask(); - if(task) - { - qDebug() << "Loading remote meta patch" << id; - m_tasks.append(task.unwrap()); - } - } - else - { - qDebug() << "Ignoring local patch" << id; - } + m_tasks.append(task.unwrap()); } } @@ -90,10 +74,7 @@ OneSixUpdate::OneSixUpdate(MinecraftInstance *inst, QObject *parent) : Task(pare { m_tasks.append(std::make_shared(m_inst)); } -} -void OneSixUpdate::executeTask() -{ if(!m_preFailure.isEmpty()) { emitFailed(m_preFailure); diff --git a/api/logic/minecraft/OneSixVersionFormat.cpp b/api/logic/minecraft/OneSixVersionFormat.cpp index 7ebf514f..d91eae58 100644 --- a/api/logic/minecraft/OneSixVersionFormat.cpp +++ b/api/logic/minecraft/OneSixVersionFormat.cpp @@ -192,6 +192,19 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc out->mainJar = lib; } + if (root.contains("requires")) + { + Meta::parseRequires(root, &out->requires); + } + if (root.contains("conflicts")) + { + Meta::parseRequires(root, &out->conflicts); + } + if (root.contains("volatile")) + { + out->m_volatile = requireBoolean(root, "volatile"); + } + /* removed features that shouldn't be used */ if (root.contains("tweakers")) { @@ -216,13 +229,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc return out; } -QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch, bool saveOrder) +QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch) { QJsonObject root; - if (saveOrder) - { - root.insert("order", patch->order); - } writeString(root, "name", patch->name); writeString(root, "uid", patch->uid); @@ -266,6 +275,18 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } root.insert("mods", array); } + if(!patch->requires.empty()) + { + Meta::serializeRequires(root, &patch->requires, "requires"); + } + if(!patch->conflicts.empty()) + { + Meta::serializeRequires(root, &patch->conflicts, "conflicts"); + } + if(patch->m_volatile) + { + root.insert("volatile", true); + } // write the contents to a json document. { QJsonDocument out; diff --git a/api/logic/minecraft/OneSixVersionFormat.h b/api/logic/minecraft/OneSixVersionFormat.h index 2306ac8e..8b782db0 100644 --- a/api/logic/minecraft/OneSixVersionFormat.h +++ b/api/logic/minecraft/OneSixVersionFormat.h @@ -10,7 +10,7 @@ class OneSixVersionFormat public: // version files / profile patches static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder); - static QJsonDocument versionFileToJson(const VersionFilePtr &patch, bool saveOrder); + static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); diff --git a/api/logic/minecraft/ProfilePatch.cpp b/api/logic/minecraft/ProfilePatch.cpp deleted file mode 100644 index b7a5a37a..00000000 --- a/api/logic/minecraft/ProfilePatch.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include -#include -#include -#include "ProfilePatch.h" - -#include "meta/Version.h" -#include "VersionFile.h" -#include "minecraft/ComponentList.h" - -ProfilePatch::ProfilePatch(std::shared_ptr version) - :m_metaVersion(version) -{ -} - -ProfilePatch::ProfilePatch(std::shared_ptr file, const QString& filename) - :m_file(file), m_filename(filename) -{ -} - -std::shared_ptr ProfilePatch::getMeta() -{ - return m_metaVersion; -} - -void ProfilePatch::applyTo(LaunchProfile* profile) -{ - auto vfile = getVersionFile(); - if(vfile) - { - vfile->applyTo(profile); - } - else - { - profile->applyProblemSeverity(getProblemSeverity()); - } -} - -std::shared_ptr ProfilePatch::getVersionFile() const -{ - if(m_metaVersion) - { - if(!m_metaVersion->isLoaded()) - { - m_metaVersion->load(); - } - return m_metaVersion->data(); - } - else - { - return m_file; - } -} - -std::shared_ptr ProfilePatch::getVersionList() const -{ - if(m_metaVersion) - { - return ENV.metadataIndex()->get(m_metaVersion->uid()); - } - return nullptr; -} - -int ProfilePatch::getOrder() -{ - if(m_orderOverride) - return m_order; - - auto vfile = getVersionFile(); - if(vfile) - { - return vfile->order; - } - return 0; -} -void ProfilePatch::setOrder(int order) -{ - m_orderOverride = true; - m_order = order; -} -QString ProfilePatch::getID() -{ - if(m_metaVersion) - return m_metaVersion->uid(); - return getVersionFile()->uid; -} -QString ProfilePatch::getName() -{ - if(m_metaVersion) - return m_metaVersion->name(); - return getVersionFile()->name; -} -QString ProfilePatch::getVersion() -{ - if(m_metaVersion) - return m_metaVersion->version(); - return getVersionFile()->version; -} -QString ProfilePatch::getFilename() -{ - return m_filename; -} -QDateTime ProfilePatch::getReleaseDateTime() -{ - if(m_metaVersion) - { - return m_metaVersion->time(); - } - return getVersionFile()->releaseTime; -} - -bool ProfilePatch::isCustom() -{ - return !m_isVanilla; -}; - -bool ProfilePatch::isCustomizable() -{ - if(m_metaVersion) - { - if(getVersionFile()) - { - return true; - } - } - return false; -} -bool ProfilePatch::isRemovable() -{ - return m_isRemovable; -} -bool ProfilePatch::isRevertible() -{ - return m_isRevertible; -} -bool ProfilePatch::isMoveable() -{ - return m_isMovable; -} -bool ProfilePatch::isVersionChangeable() -{ - auto list = getVersionList(); - if(list) - { - if(!list->isLoaded()) - { - list->load(); - } - return list->count() != 0; - } - return false; -} - -void ProfilePatch::setVanilla (bool state) -{ - m_isVanilla = state; -} -void ProfilePatch::setRemovable (bool state) -{ - m_isRemovable = state; -} -void ProfilePatch::setRevertible (bool state) -{ - m_isRevertible = state; -} -void ProfilePatch::setMovable (bool state) -{ - m_isMovable = state; -} - -ProblemSeverity ProfilePatch::getProblemSeverity() const -{ - auto file = getVersionFile(); - if(file) - { - return file->getProblemSeverity(); - } - return ProblemSeverity::Error; -} - -const QList ProfilePatch::getProblems() const -{ - auto file = getVersionFile(); - if(file) - { - return file->getProblems(); - } - return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}}; -} diff --git a/api/logic/minecraft/ProfilePatch.h b/api/logic/minecraft/ProfilePatch.h deleted file mode 100644 index 80825342..00000000 --- a/api/logic/minecraft/ProfilePatch.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "ProblemProvider.h" - -class ComponentList; -class LaunchProfile; -namespace Meta -{ - class Version; - class VersionList; -} -class VersionFile; - -class ProfilePatch : public ProblemProvider -{ -public: - ProfilePatch(std::shared_ptr version); - ProfilePatch(std::shared_ptr file, const QString &filename = QString()); - - virtual ~ProfilePatch(){}; - virtual void applyTo(LaunchProfile *profile); - - virtual bool isMoveable(); - virtual bool isCustomizable(); - virtual bool isRevertible(); - virtual bool isRemovable(); - virtual bool isCustom(); - virtual bool isVersionChangeable(); - - virtual void setOrder(int order); - virtual int getOrder(); - - virtual QString getID(); - virtual QString getName(); - virtual QString getVersion(); - virtual std::shared_ptr getMeta(); - virtual QDateTime getReleaseDateTime(); - - virtual QString getFilename(); - - virtual std::shared_ptr getVersionFile() const; - virtual std::shared_ptr getVersionList() const; - - void setVanilla (bool state); - void setRemovable (bool state); - void setRevertible (bool state); - void setMovable (bool state); - - const QList getProblems() const override; - ProblemSeverity getProblemSeverity() const override; - -protected: - // Properties for UI and version manipulation from UI in general - bool m_isMovable = false; - bool m_isRevertible = false; - bool m_isRemovable = false; - bool m_isVanilla = false; - - bool m_orderOverride = false; - int m_order = 0; - - std::shared_ptr m_metaVersion; - std::shared_ptr m_file; - QString m_filename; -}; - -typedef std::shared_ptr ProfilePatchPtr; diff --git a/api/logic/minecraft/ProfileUtils.cpp b/api/logic/minecraft/ProfileUtils.cpp index c8722f7e..a6d2028d 100644 --- a/api/logic/minecraft/ProfileUtils.cpp +++ b/api/logic/minecraft/ProfileUtils.cpp @@ -14,38 +14,6 @@ namespace ProfileUtils static const int currentOrderFileVersion = 1; -bool writeOverrideOrders(QString path, const PatchOrder &order) -{ - QJsonObject obj; - obj.insert("version", currentOrderFileVersion); - QJsonArray orderArray; - for(auto str: order) - { - orderArray.append(str); - } - obj.insert("order", orderArray); - QSaveFile orderFile(path); - if (!orderFile.open(QFile::WriteOnly)) - { - qCritical() << "Couldn't open" << orderFile.fileName() - << "for writing:" << orderFile.errorString(); - return false; - } - auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); - if(orderFile.write(data) != data.size()) - { - qCritical() << "Couldn't write all the data into" << orderFile.fileName() - << "because:" << orderFile.errorString(); - return false; - } - if(!orderFile.commit()) - { - qCritical() << "Couldn't save" << orderFile.fileName() - << "because:" << orderFile.errorString(); - } - return true; -} - bool readOverrideOrders(QString path, PatchOrder &order) { QFile orderFile(path); @@ -154,6 +122,25 @@ VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder) return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); } +bool saveJsonFile(const QJsonDocument doc, const QString & filename) +{ + auto data = doc.toJson(); + QSaveFile jsonFile(filename); + if(!jsonFile.open(QIODevice::WriteOnly)) + { + jsonFile.cancelWriting(); + qWarning() << "Couldn't open" << filename << "for writing"; + return false; + } + jsonFile.write(data); + if(!jsonFile.commit()) + { + qWarning() << "Couldn't save" << filename; + return false; + } + return true; +} + VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo) { QFile file(fileInfo.absoluteFilePath()); diff --git a/api/logic/minecraft/ProfileUtils.h b/api/logic/minecraft/ProfileUtils.h index 267fd42b..351c36cb 100644 --- a/api/logic/minecraft/ProfileUtils.h +++ b/api/logic/minecraft/ProfileUtils.h @@ -16,6 +16,9 @@ bool writeOverrideOrders(QString path, const PatchOrder &order); /// Parse a version file in JSON format VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder); +/// Save a JSON file (in any format) +bool saveJsonFile(const QJsonDocument doc, const QString & filename); + /// Parse a version file in binary JSON format VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo); diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h index 60acaad6..c032f7ea 100644 --- a/api/logic/minecraft/VersionFile.h +++ b/api/logic/minecraft/VersionFile.h @@ -10,6 +10,7 @@ #include "minecraft/Rule.h" #include "ProblemProvider.h" #include "Library.h" +#include class ComponentList; class VersionFile; @@ -29,9 +30,6 @@ public: /* data */ /// MultiMC: order hint for this version file if no explicit order is set int order = 0; - /// MultiMC: filename of the file this was loaded from - // QString filename; - /// MultiMC: human readable name of this package QString name; @@ -77,7 +75,7 @@ public: /* data */ /// Mojang: list of libraries to add to the version QList libraries; - // The main jar (Minecraft version library, normally) + /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; /// MultiMC: list of attached traits of this version file - used to enable features @@ -89,6 +87,21 @@ public: /* data */ /// MultiMC: list of mods added to this version QList mods; + /** + * MultiMC: set of packages this depends on + * NOTE: this is shared with the meta format!!! + */ + Meta::RequireSet requires; + + /** + * MultiMC: set of packages this conflicts with + * NOTE: this is shared with the meta format!!! + */ + Meta::RequireSet conflicts; + + /// is volatile -- may be removed as soon as it is no longer needed by something else + bool m_volatile = false; + public: // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. QMap > mojangDownloads; diff --git a/api/logic/minecraft/launch/ModMinecraftJar.cpp b/api/logic/minecraft/launch/ModMinecraftJar.cpp index 6407ade0..9b5d3b20 100644 --- a/api/logic/minecraft/launch/ModMinecraftJar.cpp +++ b/api/logic/minecraft/launch/ModMinecraftJar.cpp @@ -25,6 +25,11 @@ void ModMinecraftJar::executeTask() { auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + if(!m_inst->getJarMods().size()) + { + emitSucceeded(); + return; + } // nuke obsolete stripped jar(s) if needed if(!FS::ensureFolderPathExists(m_inst->binRoot())) { diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/api/logic/minecraft/legacy/LegacyInstance.cpp index 0b69acf2..a6b745d8 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.cpp +++ b/api/logic/minecraft/legacy/LegacyInstance.cpp @@ -71,7 +71,7 @@ bool LegacyInstance::shouldUseCustomBaseJar() const } -shared_qobject_ptr LegacyInstance::createUpdateTask() +shared_qobject_ptr LegacyInstance::createUpdateTask(Net::Mode) { return nullptr; } diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h index 64564591..ef590cae 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -93,7 +93,7 @@ public: }; virtual bool shouldUpdate() const; - virtual shared_qobject_ptr createUpdateTask() override; + virtual shared_qobject_ptr createUpdateTask(Net::Mode mode) override; virtual QString typeName() const override; diff --git a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp index fab48005..6cda3e4d 100644 --- a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp +++ b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp @@ -67,69 +67,70 @@ void LegacyUpgradeTask::copyFinished() auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); - std::shared_ptr inst(new MinecraftInstance(m_globalSettings, instanceSettings, m_stagingPath)); - inst->setName(m_newName); - inst->init(); - - QString preferredVersionNumber = decideVersion(legacyInst->currentVersionId(), legacyInst->intendedVersionId()); - if(preferredVersionNumber.isNull()) + // NOTE: this scope ensures the instance is fully saved before we emitSucceeded { - // try to decide version based on the jar(s?) - preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->baseJar()); + MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); + inst.setName(m_newName); + inst.init(); + + QString preferredVersionNumber = decideVersion(legacyInst->currentVersionId(), legacyInst->intendedVersionId()); if(preferredVersionNumber.isNull()) { - preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->runnableJar()); + // try to decide version based on the jar(s?) + preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->baseJar()); if(preferredVersionNumber.isNull()) { - emitFailed(tr("Could not decide Minecraft version.")); - return; + preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->runnableJar()); + if(preferredVersionNumber.isNull()) + { + emitFailed(tr("Could not decide Minecraft version.")); + return; + } } } - } - inst->setComponentVersion("net.minecraft", preferredVersionNumber); + auto components = inst.getComponentList(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", preferredVersionNumber, true); - // BUG: reloadProfile should not be necessary, but setComponentVersion voids the profile created by init()! - inst->reloadProfile(); - auto profile = inst->getComponentList(); - - if(legacyInst->shouldUseCustomBaseJar()) - { - QString jarPath = legacyInst->customBaseJar(); - qDebug() << "Base jar is custom! : " << jarPath; - // FIXME: handle case when the jar is unreadable? - // TODO: check the hash, if it's the same as the upstream jar, do not do this - profile->installCustomJar(jarPath); - } - - auto jarMods = legacyInst->getJarMods(); - for(auto & jarMod: jarMods) - { - QString modPath = jarMod.filename().absoluteFilePath(); - qDebug() << "jarMod: " << modPath; - profile->installJarMods({modPath}); - } - - // remove all the extra garbage we no longer need - auto removeAll = [&](const QString &root, const QStringList &things) - { - for(auto &thing : things) + if(legacyInst->shouldUseCustomBaseJar()) { - auto removePath = FS::PathCombine(root, thing); - QFileInfo stat(removePath); - if(stat.isDir()) - { - FS::deletePath(removePath); - } - else - { - QFile::remove(removePath); - } + QString jarPath = legacyInst->customBaseJar(); + qDebug() << "Base jar is custom! : " << jarPath; + // FIXME: handle case when the jar is unreadable? + // TODO: check the hash, if it's the same as the upstream jar, do not do this + components->installCustomJar(jarPath); } - }; - QStringList rootRemovables = {"modlist", "version", "instMods"}; - QStringList mcRemovables = {"bin", "MultiMCLauncher.jar", "icon.png"}; - removeAll(inst->instanceRoot(), rootRemovables); - removeAll(inst->minecraftRoot(), mcRemovables); + + auto jarMods = legacyInst->getJarMods(); + for(auto & jarMod: jarMods) + { + QString modPath = jarMod.filename().absoluteFilePath(); + qDebug() << "jarMod: " << modPath; + components->installJarMods({modPath}); + } + + // remove all the extra garbage we no longer need + auto removeAll = [&](const QString &root, const QStringList &things) + { + for(auto &thing : things) + { + auto removePath = FS::PathCombine(root, thing); + QFileInfo stat(removePath); + if(stat.isDir()) + { + FS::deletePath(removePath); + } + else + { + QFile::remove(removePath); + } + } + }; + QStringList rootRemovables = {"modlist", "version", "instMods"}; + QStringList mcRemovables = {"bin", "MultiMCLauncher.jar", "icon.png"}; + removeAll(inst.instanceRoot(), rootRemovables); + removeAll(inst.minecraftRoot(), mcRemovables); + } emitSucceeded(); } diff --git a/api/logic/minecraft/update/FMLLibrariesTask.cpp b/api/logic/minecraft/update/FMLLibrariesTask.cpp index 56ecee43..1bd339e4 100644 --- a/api/logic/minecraft/update/FMLLibrariesTask.cpp +++ b/api/logic/minecraft/update/FMLLibrariesTask.cpp @@ -15,7 +15,6 @@ void FMLLibrariesTask::executeTask() MinecraftInstance *inst = (MinecraftInstance *)m_inst; auto components = inst->getComponentList(); auto profile = components->getProfile(); - bool forge_present = false; if (!profile->hasTrait("legacyFML")) { @@ -23,7 +22,7 @@ void FMLLibrariesTask::executeTask() return; } - QString version = inst->getComponentVersion("net.minecraft"); + QString version = components->getComponentVersion("net.minecraft"); auto &fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; if (!fmlLibsMapping.contains(version)) { @@ -35,9 +34,7 @@ void FMLLibrariesTask::executeTask() // determine if we need some libs for FML or forge setStatus(tr("Checking for FML libraries...")); - forge_present = (components->versionPatch("net.minecraftforge") != nullptr); - // we don't... - if (!forge_present) + if(!components->getComponent("net.minecraftforge")) { emitSucceeded(); return; diff --git a/api/logic/minecraft/update/LibrariesTask.cpp b/api/logic/minecraft/update/LibrariesTask.cpp index 80d45d97..0bec61c1 100644 --- a/api/logic/minecraft/update/LibrariesTask.cpp +++ b/api/logic/minecraft/update/LibrariesTask.cpp @@ -13,12 +13,6 @@ void LibrariesTask::executeTask() setStatus(tr("Getting the library files from Mojang...")); qDebug() << m_inst->name() << ": downloading libraries"; MinecraftInstance *inst = (MinecraftInstance *)m_inst; - inst->reloadProfile(); - if(inst->hasVersionBroken()) - { - emitFailed(tr("Failed to load the version description files - check the instance for errors.")); - return; - } // Build a list of URLs that will need to be downloaded. auto components = inst->getComponentList(); diff --git a/api/logic/net/Mode.h b/api/logic/net/Mode.h new file mode 100644 index 00000000..62e26d92 --- /dev/null +++ b/api/logic/net/Mode.h @@ -0,0 +1,10 @@ +#pragma once + +namespace Net +{ +enum class Mode +{ + Offline, + Online +}; +} diff --git a/application/LaunchController.cpp b/application/LaunchController.cpp index d451e652..70b71eaf 100644 --- a/application/LaunchController.cpp +++ b/application/LaunchController.cpp @@ -192,7 +192,7 @@ void LaunchController::launchInstance() Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); - if(!m_instance->reload()) + if(!m_instance->reloadSettings()) { QMessageBox::critical(m_parentWidget, tr("Error"), tr("Couldn't load the instance profile.")); emitFailed(tr("Couldn't load the instance profile.")); diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index 908a126c..9f9772c8 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -1303,7 +1303,7 @@ void MainWindow::finalizeInstance(InstancePtr inst) if (MMC->accounts()->anyAccountIsValid()) { ProgressDialog loadDialog(this); - auto update = inst->createUpdateTask(); + auto update = inst->createUpdateTask(Net::Mode::Online); connect(update.get(), &Task::failed, [this](QString reason) { QString error = QString("Instance load failed: %1").arg(reason); diff --git a/application/dialogs/NewInstanceDialog.cpp b/application/dialogs/NewInstanceDialog.cpp index d1a2bbfa..9fa20856 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/application/dialogs/NewInstanceDialog.cpp @@ -71,7 +71,7 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString } else { - vlist->load(); + vlist->load(Net::Mode::Online); auto task = vlist->getLoadTask(); if(vlist->isLoaded()) { diff --git a/application/pages/ModFolderPage.cpp b/application/pages/ModFolderPage.cpp index be1c8289..4ef5d71c 100644 --- a/application/pages/ModFolderPage.cpp +++ b/application/pages/ModFolderPage.cpp @@ -106,15 +106,15 @@ bool CoreModFolderPage::shouldDisplay() const auto version = inst->getComponentList(); if (!version) return true; - if(!version->versionPatch("net.minecraftforge")) + if(!version->getComponent("net.minecraftforge")) { return false; } - if(!version->versionPatch("net.minecraft")) + if(!version->getComponent("net.minecraft")) { return false; } - if(version->versionPatch("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) + if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) { return true; } diff --git a/application/pages/VersionPage.cpp b/application/pages/VersionPage.cpp index d65d6bc7..c9f3453f 100644 --- a/application/pages/VersionPage.cpp +++ b/application/pages/VersionPage.cpp @@ -103,10 +103,10 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); + m_profile = m_inst->getComponentList(); reloadComponentList(); - m_profile = m_inst->getComponentList(); if (m_profile) { auto proxy = new IconProxy(ui->packageView); @@ -142,7 +142,7 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex & return; } int row = current.row(); - auto patch = m_profile->versionPatch(row); + auto patch = m_profile->getComponent(row); auto severity = patch->getProblemSeverity(); switch(severity) { @@ -196,7 +196,7 @@ bool VersionPage::reloadComponentList() { try { - m_inst->reloadProfile(); + m_profile->reload(Net::Mode::Online); return true; } catch (Exception &e) @@ -262,19 +262,6 @@ void VersionPage::on_jarBtn_clicked() updateButtons(); } -void VersionPage::on_resetOrderBtn_clicked() -{ - try - { - m_profile->resetOrder(); - } - catch (Exception &e) - { - QMessageBox::critical(this, tr("Error"), e.cause()); - } - updateButtons(); -} - void VersionPage::on_moveUpBtn_clicked() { try @@ -308,7 +295,7 @@ void VersionPage::on_changeVersionBtn_clicked() { return; } - auto patch = m_profile->versionPatch(versionRow); + auto patch = m_profile->getComponent(versionRow); auto name = patch->getName(); auto list = patch->getVersionList(); if(!list) @@ -331,8 +318,10 @@ void VersionPage::on_changeVersionBtn_clicked() } qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor(); + bool important = false; if(uid == "net.minecraft") { + important = true; if (!m_profile->isVanilla()) { auto result = CustomMessageBox::selectable( @@ -348,14 +337,14 @@ void VersionPage::on_changeVersionBtn_clicked() reloadComponentList(); } } - m_inst->setComponentVersion(uid, vselect.selectedVersion()->descriptor()); + m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); doUpdate(); m_container->refreshContainer(); } int VersionPage::doUpdate() { - auto updateTask = m_inst->createUpdateTask(); + auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); if (!updateTask) { return 1; @@ -376,20 +365,58 @@ void VersionPage::on_forgeBtn_clicked() return; } VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this); - vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_inst->getComponentVersion("net.minecraft")); - vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_inst->getComponentVersion("net.minecraft")); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!")); if (vselect.exec() && vselect.selectedVersion()) { auto vsn = vselect.selectedVersion(); - m_inst->setComponentVersion("net.minecraftforge", vsn->descriptor()); - m_profile->reload(); + m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); // m_profile->installVersion(); preselect(m_profile->rowCount(QModelIndex())-1); m_container->refreshContainer(); } } +// TODO: use something like this... except the final decision of what to show has to be deferred until the lists are known +/* +void VersionPage::on_liteloaderBtn_clicked() +{ + QString uid = "com.mumfrey.liteloader"; + auto vlist = ENV.metadataIndex()->get(uid); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select %1 version").arg(vlist->name()), this); + auto parentUid = vlist->parentUid(); + if(!parentUid.isEmpty()) + { + auto parentvlist = ENV.metadataIndex()->get(parentUid); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion(parentUid)); + vselect.setEmptyString( + tr("No %1 versions are currently available for %2 %3") + .arg(vlist->name()) + .arg(parentvlist->name()) + .arg(m_profile->getComponentVersion(parentUid))); + } + else + { + vselect.setEmptyString(tr("No %1 versions are currently available")); + } + vselect.setEmptyErrorString(tr("Couldn't load or download the %1 version lists!").arg(vlist->name())); + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion(uid, vsn->descriptor()); + m_profile->resolve(); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} +*/ + void VersionPage::on_liteloaderBtn_clicked() { auto vlist = ENV.metadataIndex()->get("com.mumfrey.liteloader"); @@ -398,14 +425,14 @@ void VersionPage::on_liteloaderBtn_clicked() return; } VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this); - vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_inst->getComponentVersion("net.minecraft")); - vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_inst->getComponentVersion("net.minecraft")); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!")); if (vselect.exec() && vselect.selectedVersion()) { auto vsn = vselect.selectedVersion(); - m_inst->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); - m_profile->reload(); + m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); // m_profile->installVersion(vselect.selectedVersion()); preselect(m_profile->rowCount(QModelIndex())-1); m_container->refreshContainer(); @@ -441,7 +468,7 @@ void VersionPage::updateButtons(int row) { if(row == -1) row = currentRow(); - auto patch = m_profile->versionPatch(row); + auto patch = m_profile->getComponent(row); if (!patch) { ui->removeBtn->setDisabled(true); @@ -470,14 +497,14 @@ void VersionPage::onGameUpdateError(QString error) QMessageBox::Warning)->show(); } -ProfilePatchPtr VersionPage::current() +ComponentPtr VersionPage::current() { auto row = currentRow(); if(row < 0) { return nullptr; } - return m_profile->versionPatch(row); + return m_profile->getComponent(row); } int VersionPage::currentRow() @@ -496,7 +523,7 @@ void VersionPage::on_customizeBtn_clicked() { return; } - auto patch = m_profile->versionPatch(version); + auto patch = m_profile->getComponent(version); if(!patch->getVersionFile()) { // TODO: wait for the update task to finish here... diff --git a/application/pages/VersionPage.h b/application/pages/VersionPage.h index 0ac10a14..809b2c0c 100644 --- a/application/pages/VersionPage.h +++ b/application/pages/VersionPage.h @@ -53,7 +53,6 @@ private slots: void on_liteloaderBtn_clicked(); void on_reloadBtn_clicked(); void on_removeBtn_clicked(); - void on_resetOrderBtn_clicked(); void on_moveUpBtn_clicked(); void on_moveDownBtn_clicked(); void on_jarmodBtn_clicked(); @@ -68,7 +67,7 @@ private slots: void on_changeVersionBtn_clicked(); private: - ProfilePatchPtr current(); + ComponentPtr current(); int currentRow(); void updateButtons(int row = -1); void preselect(int row = 0); diff --git a/application/pages/VersionPage.ui b/application/pages/VersionPage.ui index afb33164..b6da3294 100644 --- a/application/pages/VersionPage.ui +++ b/application/pages/VersionPage.ui @@ -7,7 +7,7 @@ 0 0 693 - 788 + 833 @@ -217,16 +217,6 @@ - - - - Reset apply order of packages. - - - Reset order - - - @@ -300,7 +290,6 @@ liteloaderBtn modBtn jarmodBtn - resetOrderBtn reloadBtn diff --git a/application/pages/global/PackagesPage.cpp b/application/pages/global/PackagesPage.cpp index e15ddbab..7b117d0b 100644 --- a/application/pages/global/PackagesPage.cpp +++ b/application/pages/global/PackagesPage.cpp @@ -38,10 +38,17 @@ static QString formatRequires(const VersionPtr &version) auto iter = reqs.begin(); while (iter != reqs.end()) { - auto &uid = iter.key(); - auto &version = iter.value(); + auto &uid = iter->uid; + auto &version = iter->equalsVersion; const QString readable = ENV.metadataIndex()->hasUid(uid) ? ENV.metadataIndex()->get(uid)->humanReadable() : uid; - lines.append(QString("%1 (%2)").arg(readable, version)); + if(!version.isEmpty()) + { + lines.append(QString("%1 (%2)").arg(readable, version)); + } + else + { + lines.append(QString("%1").arg(readable)); + } iter++; } return lines.join('\n'); @@ -95,7 +102,7 @@ QIcon PackagesPage::icon() const void PackagesPage::on_refreshIndexBtn_clicked() { - ENV.metadataIndex()->load(); + ENV.metadataIndex()->load(Net::Mode::Online); } void PackagesPage::on_refreshFileBtn_clicked() { @@ -104,7 +111,7 @@ void PackagesPage::on_refreshFileBtn_clicked() { return; } - list->load(); + list->load(Net::Mode::Online); } void PackagesPage::on_refreshVersionBtn_clicked() { @@ -113,7 +120,7 @@ void PackagesPage::on_refreshVersionBtn_clicked() { return; } - version->load(); + version->load(Net::Mode::Online); } void PackagesPage::on_fileSearchEdit_textChanged(const QString &search) @@ -156,7 +163,7 @@ void PackagesPage::updateCurrentVersionList(const QModelIndex &index) ui->fileName->setText(list->name()); m_versionProxy->setSourceModel(list.get()); ui->refreshFileBtn->setText(tr("Refresh %1").arg(list->humanReadable())); - list->load(); + list->load(Net::Mode::Offline); } else { @@ -213,5 +220,5 @@ void PackagesPage::updateVersion() void PackagesPage::opened() { - ENV.metadataIndex()->load(); + ENV.metadataIndex()->load(Net::Mode::Offline); }