diff options
Diffstat (limited to 'launcher/meta')
-rw-r--r-- | launcher/meta/BaseEntity.cpp | 168 | ||||
-rw-r--r-- | launcher/meta/BaseEntity.h | 67 | ||||
-rw-r--r-- | launcher/meta/Index.cpp | 148 | ||||
-rw-r--r-- | launcher/meta/Index.h | 69 | ||||
-rw-r--r-- | launcher/meta/Index_test.cpp | 44 | ||||
-rw-r--r-- | launcher/meta/JsonFormat.cpp | 218 | ||||
-rw-r--r-- | launcher/meta/JsonFormat.h | 83 | ||||
-rw-r--r-- | launcher/meta/Version.cpp | 140 | ||||
-rw-r--r-- | launcher/meta/Version.h | 116 | ||||
-rw-r--r-- | launcher/meta/VersionList.cpp | 245 | ||||
-rw-r--r-- | launcher/meta/VersionList.h | 101 |
11 files changed, 1399 insertions, 0 deletions
diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp new file mode 100644 index 00000000..5ff7a59a --- /dev/null +++ b/launcher/meta/BaseEntity.cpp @@ -0,0 +1,168 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseEntity.h" + +#include "Json.h" + +#include "net/Download.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" + +#include "Env.h" +#include "Json.h" + +#include "BuildConfig.h" + +class ParsingValidator : public Net::Validator +{ +public: /* con/des */ + ParsingValidator(Meta::BaseEntity *entity) : m_entity(entity) + { + }; + virtual ~ParsingValidator() + { + }; + +public: /* methods */ + bool init(QNetworkRequest &) override + { + return true; + } + bool write(QByteArray & data) override + { + this->data.append(data); + return true; + } + bool abort() override + { + return true; + } + bool validate(QNetworkReply &) override + { + auto fname = m_entity->localFilename(); + try + { + auto doc = Json::requireDocument(data, fname); + auto obj = Json::requireObject(doc, fname); + m_entity->parse(obj); + return true; + } + catch (const Exception &e) + { + qWarning() << "Unable to parse response:" << e.cause(); + return false; + } + } + +private: /* data */ + QByteArray data; + Meta::BaseEntity *m_entity; +}; + +Meta::BaseEntity::~BaseEntity() +{ +} + +QUrl Meta::BaseEntity::url() const +{ + return QUrl(BuildConfig.META_URL).resolved(localFilename()); +} + +bool Meta::BaseEntity::loadLocalFile() +{ + const QString fname = QDir("meta").absoluteFilePath(localFilename()); + if (!QFile::exists(fname)) + { + return false; + } + // TODO: check if the file has the expected checksum + try + { + auto doc = Json::requireDocument(fname, fname); + auto obj = Json::requireObject(doc, fname); + parse(obj); + return true; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); + // just make sure it's gone and we never consider it again. + QFile::remove(fname); + return false; + } +} + +void Meta::BaseEntity::load(Net::Mode loadType) +{ + // load local file if nothing is loaded yet + if(!isLoaded()) + { + if(loadLocalFile()) + { + m_loadStatus = LoadStatus::Local; + } + } + // if we need remote update, run the update task + if(loadType == Net::Mode::Offline || !shouldStartRemoteUpdate()) + { + return; + } + NetJob *job = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename())); + auto url = this->url(); + auto entry = ENV.metacache()->resolveEntry("meta", localFilename()); + entry->setStale(true); + auto dl = Net::Download::makeCached(url, entry); + /* + * The validator parses the file and loads it into the object. + * If that fails, the file is not written to storage. + */ + dl->addValidator(new ParsingValidator(this)); + job->addNetAction(dl); + m_updateStatus = UpdateStatus::InProgress; + m_updateTask.reset(job); + QObject::connect(job, &NetJob::succeeded, [&]() + { + m_loadStatus = LoadStatus::Remote; + m_updateStatus = UpdateStatus::Succeeded; + m_updateTask.reset(); + }); + QObject::connect(job, &NetJob::failed, [&]() + { + m_updateStatus = UpdateStatus::Failed; + m_updateTask.reset(); + }); + m_updateTask->start(); +} + +bool Meta::BaseEntity::isLoaded() const +{ + return m_loadStatus > LoadStatus::NotLoaded; +} + +bool Meta::BaseEntity::shouldStartRemoteUpdate() const +{ + // TODO: version-locks and offline mode? + return m_updateStatus != UpdateStatus::InProgress; +} + +shared_qobject_ptr<Task> Meta::BaseEntity::getCurrentTask() +{ + if(m_updateStatus == UpdateStatus::InProgress) + { + return m_updateTask; + } + return nullptr; +} diff --git a/launcher/meta/BaseEntity.h b/launcher/meta/BaseEntity.h new file mode 100644 index 00000000..eff43879 --- /dev/null +++ b/launcher/meta/BaseEntity.h @@ -0,0 +1,67 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QJsonObject> +#include <QObject> +#include "QObjectPtr.h" + +#include "net/Mode.h" + +class Task; +namespace Meta +{ +class BaseEntity +{ +public: /* types */ + using Ptr = std::shared_ptr<BaseEntity>; + enum class LoadStatus + { + NotLoaded, + Local, + Remote + }; + enum class UpdateStatus + { + NotDone, + InProgress, + Failed, + Succeeded + }; + +public: + virtual ~BaseEntity(); + + virtual void parse(const QJsonObject &obj) = 0; + + virtual QString localFilename() const = 0; + virtual QUrl url() const; + + bool isLoaded() const; + bool shouldStartRemoteUpdate() const; + + void load(Net::Mode loadType); + shared_qobject_ptr<Task> getCurrentTask(); + +protected: /* methods */ + bool loadLocalFile(); + +private: + LoadStatus m_loadStatus = LoadStatus::NotLoaded; + UpdateStatus m_updateStatus = UpdateStatus::NotDone; + shared_qobject_ptr<Task> m_updateTask; +}; +} diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp new file mode 100644 index 00000000..6802470d --- /dev/null +++ b/launcher/meta/Index.cpp @@ -0,0 +1,148 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Index.h" + +#include "VersionList.h" +#include "JsonFormat.h" + +namespace Meta +{ +Index::Index(QObject *parent) + : QAbstractListModel(parent) +{ +} +Index::Index(const QVector<VersionListPtr> &lists, QObject *parent) + : QAbstractListModel(parent), m_lists(lists) +{ + for (int i = 0; i < m_lists.size(); ++i) + { + m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); + connectVersionList(i, m_lists.at(i)); + } +} + +QVariant Index::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size()) + { + return QVariant(); + } + + VersionListPtr list = m_lists.at(index.row()); + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case 0: return list->humanReadable(); + default: break; + } + case UidRole: return list->uid(); + case NameRole: return list->name(); + case ListPtrRole: return QVariant::fromValue(list); + } + return QVariant(); +} +int Index::rowCount(const QModelIndex &parent) const +{ + return m_lists.size(); +} +int Index::columnCount(const QModelIndex &parent) const +{ + return 1; +} +QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) + { + return tr("Name"); + } + else + { + return QVariant(); + } +} + +bool Index::hasUid(const QString &uid) const +{ + return m_uids.contains(uid); +} + +VersionListPtr Index::get(const QString &uid) +{ + VersionListPtr out = m_uids.value(uid, nullptr); + if(!out) + { + out = std::make_shared<VersionList>(uid); + m_uids[uid] = out; + } + return out; +} + +VersionPtr Index::get(const QString &uid, const QString &version) +{ + auto list = get(uid); + return list->getVersion(version); +} + +void Index::parse(const QJsonObject& obj) +{ + parseIndex(obj, this); +} + +void Index::merge(const std::shared_ptr<Index> &other) +{ + const QVector<VersionListPtr> lists = std::dynamic_pointer_cast<Index>(other)->m_lists; + // initial load, no need to merge + if (m_lists.isEmpty()) + { + beginResetModel(); + m_lists = lists; + for (int i = 0; i < lists.size(); ++i) + { + m_uids.insert(lists.at(i)->uid(), lists.at(i)); + connectVersionList(i, lists.at(i)); + } + endResetModel(); + } + else + { + for (const VersionListPtr &list : lists) + { + if (m_uids.contains(list->uid())) + { + m_uids[list->uid()]->mergeFromIndex(list); + } + else + { + beginInsertRows(QModelIndex(), m_lists.size(), m_lists.size()); + connectVersionList(m_lists.size(), list); + m_lists.append(list); + m_uids.insert(list->uid(), list); + endInsertRows(); + } + } + } +} + +void Index::connectVersionList(const int row, const VersionListPtr &list) +{ + connect(list.get(), &VersionList::nameChanged, this, [this, row]() + { + emit dataChanged(index(row), index(row), QVector<int>() << Qt::DisplayRole); + }); +} +} diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h new file mode 100644 index 00000000..d33ab0c8 --- /dev/null +++ b/launcher/meta/Index.h @@ -0,0 +1,69 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QAbstractListModel> +#include <memory> + +#include "BaseEntity.h" + +class Task; + +namespace Meta +{ +using VersionListPtr = std::shared_ptr<class VersionList>; +using VersionPtr = std::shared_ptr<class Version>; + +class Index : public QAbstractListModel, public BaseEntity +{ + Q_OBJECT +public: + explicit Index(QObject *parent = nullptr); + explicit Index(const QVector<VersionListPtr> &lists, QObject *parent = nullptr); + + enum + { + UidRole = Qt::UserRole, + NameRole, + ListPtrRole + }; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + QString localFilename() const override { return "index.json"; } + + // queries + VersionListPtr get(const QString &uid); + VersionPtr get(const QString &uid, const QString &version); + bool hasUid(const QString &uid) const; + + QVector<VersionListPtr> lists() const { return m_lists; } + +public: // for usage by parsers only + void merge(const std::shared_ptr<Index> &other); + void parse(const QJsonObject &obj) override; + +private: + QVector<VersionListPtr> m_lists; + QHash<QString, VersionListPtr> m_uids; + + void connectVersionList(const int row, const VersionListPtr &list); +}; +} + diff --git a/launcher/meta/Index_test.cpp b/launcher/meta/Index_test.cpp new file mode 100644 index 00000000..b0892070 --- /dev/null +++ b/launcher/meta/Index_test.cpp @@ -0,0 +1,44 @@ +#include <QTest> +#include "TestUtil.h" + +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "Env.h" + +class IndexTest : public QObject +{ + Q_OBJECT +private +slots: + void test_isProvidedByEnv() + { + QVERIFY(ENV.metadataIndex()); + QCOMPARE(ENV.metadataIndex(), ENV.metadataIndex()); + } + + void test_hasUid_and_getList() + { + Meta::Index windex({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")}); + QVERIFY(windex.hasUid("list1")); + QVERIFY(!windex.hasUid("asdf")); + QVERIFY(windex.get("list2") != nullptr); + QCOMPARE(windex.get("list2")->uid(), QString("list2")); + QVERIFY(windex.get("adsf") != nullptr); + } + + void test_merge() + { + Meta::Index windex({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")}); + QCOMPARE(windex.lists().size(), 3); + windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")}))); + QCOMPARE(windex.lists().size(), 3); + windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list4"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list5")}))); + QCOMPARE(windex.lists().size(), 5); + windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list6")}))); + QCOMPARE(windex.lists().size(), 6); + } +}; + +QTEST_GUILESS_MAIN(IndexTest) + +#include "Index_test.moc" diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp new file mode 100644 index 00000000..796da4bb --- /dev/null +++ b/launcher/meta/JsonFormat.cpp @@ -0,0 +1,218 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JsonFormat.h" + +// FIXME: remove this from here... somehow +#include "minecraft/OneSixVersionFormat.h" +#include "Json.h" + +#include "Index.h" +#include "Version.h" +#include "VersionList.h" + +using namespace Json; + +namespace Meta +{ + +MetadataVersion currentFormatVersion() +{ + return MetadataVersion::InitialRelease; +} + +// Index +static std::shared_ptr<Index> parseIndexInternal(const QJsonObject &obj) +{ + const QVector<QJsonObject> objects = requireIsArrayOf<QJsonObject>(obj, "packages"); + QVector<VersionListPtr> lists; + lists.reserve(objects.size()); + std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject &obj) + { + VersionListPtr list = std::make_shared<VersionList>(requireString(obj, "uid")); + list->setName(ensureString(obj, "name", QString())); + return list; + }); + return std::make_shared<Index>(lists); +} + +// Version +static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj) +{ + VersionPtr version = std::make_shared<Version>(uid, requireString(obj, "version")); + version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); + version->setType(ensureString(obj, "type", QString())); + version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); + version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); + RequireSet requires, conflicts; + parseRequires(obj, &requires, "requires"); + parseRequires(obj, &conflicts, "conflicts"); + version->setRequires(requires, conflicts); + return version; +} + +static std::shared_ptr<Version> parseVersionInternal(const QJsonObject &obj) +{ + VersionPtr version = parseCommonVersion(requireString(obj, "uid"), obj); + + version->setData(OneSixVersionFormat::versionFileFromJson(QJsonDocument(obj), + QString("%1/%2.json").arg(version->uid(), version->version()), + obj.contains("order"))); + return version; +} + +// Version list / package +static std::shared_ptr<VersionList> parseVersionListInternal(const QJsonObject &obj) +{ + const QString uid = requireString(obj, "uid"); + + const QVector<QJsonObject> versionsRaw = requireIsArrayOf<QJsonObject>(obj, "versions"); + QVector<VersionPtr> versions; + versions.reserve(versionsRaw.size()); + std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject &vObj) + { + auto version = parseCommonVersion(uid, vObj); + version->setProvidesRecommendations(); + return version; + }); + + VersionListPtr list = std::make_shared<VersionList>(uid); + list->setName(ensureString(obj, "name", QString())); + list->setVersions(versions); + return list; +} + + +MetadataVersion parseFormatVersion(const QJsonObject &obj, bool required) +{ + if (!obj.contains("formatVersion")) + { + if(required) + { + return MetadataVersion::Invalid; + } + return MetadataVersion::InitialRelease; + } + if (!obj.value("formatVersion").isDouble()) + { + return MetadataVersion::Invalid; + } + switch(obj.value("formatVersion").toInt()) + { + case 0: + case 1: + return MetadataVersion::InitialRelease; + default: + return MetadataVersion::Invalid; + } +} + +void serializeFormatVersion(QJsonObject& obj, Meta::MetadataVersion version) +{ + if(version == MetadataVersion::Invalid) + { + return; + } + obj.insert("formatVersion", int(version)); +} + +void parseIndex(const QJsonObject &obj, Index *ptr) +{ + const MetadataVersion version = parseFormatVersion(obj); + switch (version) + { + case MetadataVersion::InitialRelease: + ptr->merge(parseIndexInternal(obj)); + break; + case MetadataVersion::Invalid: + throw ParseException(QObject::tr("Unknown format version!")); + } +} + +void parseVersionList(const QJsonObject &obj, VersionList *ptr) +{ + const MetadataVersion version = parseFormatVersion(obj); + switch (version) + { + case MetadataVersion::InitialRelease: + ptr->merge(parseVersionListInternal(obj)); + break; + case MetadataVersion::Invalid: + throw ParseException(QObject::tr("Unknown format version!")); + } +} + +void parseVersion(const QJsonObject &obj, Version *ptr) +{ + const MetadataVersion version = parseFormatVersion(obj); + switch (version) + { + case MetadataVersion::InitialRelease: + ptr->merge(parseVersionInternal(obj)); + break; + case MetadataVersion::Invalid: + throw ParseException(QObject::tr("Unknown format version!")); + } +} + +/* +[ +{"uid":"foo", "equals":"version"} +] +*/ +void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName) +{ + if(obj.contains(keyName)) + { + QSet<QString> 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/launcher/meta/JsonFormat.h b/launcher/meta/JsonFormat.h new file mode 100644 index 00000000..93217b7e --- /dev/null +++ b/launcher/meta/JsonFormat.h @@ -0,0 +1,83 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QJsonObject> +#include <memory> + +#include "Exception.h" +#include "meta/BaseEntity.h" +#include <set> + +namespace Meta +{ +class Index; +class Version; +class VersionList; + +enum class MetadataVersion +{ + Invalid = -1, + InitialRelease = 1 +}; + +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<Require>; + +void parseIndex(const QJsonObject &obj, Index *ptr); +void parseVersion(const QJsonObject &obj, Version *ptr); +void parseVersionList(const QJsonObject &obj, VersionList *ptr); + +MetadataVersion parseFormatVersion(const QJsonObject &obj, bool required = true); +void serializeFormatVersion(QJsonObject &obj, MetadataVersion version); + +// 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"); +MetadataVersion currentFormatVersion(); +} + +Q_DECLARE_METATYPE(std::set<Meta::Require>) diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp new file mode 100644 index 00000000..a8dc3169 --- /dev/null +++ b/launcher/meta/Version.cpp @@ -0,0 +1,140 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Version.h" + +#include <QDateTime> + +#include "JsonFormat.h" +#include "minecraft/PackProfile.h" + +Meta::Version::Version(const QString &uid, const QString &version) + : BaseVersion(), m_uid(uid), m_version(version) +{ +} + +Meta::Version::~Version() +{ +} + +QString Meta::Version::descriptor() +{ + return m_version; +} +QString Meta::Version::name() +{ + if(m_data) + return m_data->name; + return m_uid; +} +QString Meta::Version::typeString() const +{ + return m_type; +} + +QDateTime Meta::Version::time() const +{ + return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC); +} + +void Meta::Version::parse(const QJsonObject& obj) +{ + parseVersion(obj, this); +} + +void Meta::Version::mergeFromList(const Meta::VersionPtr& other) +{ + if(other->m_providesRecommendations) + { + if(m_recommended != other->m_recommended) + { + setRecommended(other->m_recommended); + } + } + if (m_type != other->m_type) + { + setType(other->m_type); + } + if (m_time != other->m_time) + { + setTime(other->m_time); + } + if (m_requires != other->m_requires) + { + m_requires = other->m_requires; + } + if (m_conflicts != other->m_conflicts) + { + m_conflicts = other->m_conflicts; + } + if(m_volatile != other->m_volatile) + { + setVolatile(other->m_volatile); + } +} + +void Meta::Version::merge(const VersionPtr &other) +{ + mergeFromList(other); + if(other->m_data) + { + setData(other->m_data); + } +} + +QString Meta::Version::localFilename() const +{ + return m_uid + '/' + m_version + ".json"; +} + +void Meta::Version::setType(const QString &type) +{ + m_type = type; + emit typeChanged(); +} + +void Meta::Version::setTime(const qint64 time) +{ + m_time = time; + emit timeChanged(); +} + +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; +} + +void Meta::Version::setProvidesRecommendations() +{ + m_providesRecommendations = true; +} + +void Meta::Version::setRecommended(bool recommended) +{ + m_recommended = recommended; +} diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h new file mode 100644 index 00000000..dea8dc8a --- /dev/null +++ b/launcher/meta/Version.h @@ -0,0 +1,116 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseVersion.h" + +#include <QJsonObject> +#include <QStringList> +#include <QVector> +#include <memory> + +#include "minecraft/VersionFile.h" + +#include "BaseEntity.h" + +#include "JsonFormat.h" + +namespace Meta +{ +using VersionPtr = std::shared_ptr<class Version>; + +class Version : public QObject, public BaseVersion, public BaseEntity +{ + Q_OBJECT + +public: /* con/des */ + explicit Version(const QString &uid, const QString &version); + virtual ~Version(); + + QString descriptor() override; + QString name() override; + QString typeString() const override; + + QString uid() const + { + return m_uid; + } + QString version() const + { + return m_version; + } + QString type() const + { + return m_type; + } + QDateTime time() const; + qint64 rawTime() const + { + return m_time; + } + const Meta::RequireSet &requires() const + { + return m_requires; + } + VersionFilePtr data() const + { + return m_data; + } + bool isRecommended() const + { + return m_recommended; + } + bool isLoaded() const + { + return m_data != nullptr; + } + + void merge(const VersionPtr &other); + void mergeFromList(const VersionPtr &other); + void parse(const QJsonObject &obj) override; + + QString localFilename() const override; + +public: // for usage by format parsers only + void setType(const QString &type); + void setTime(const qint64 time); + 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); + +signals: + void typeChanged(); + void timeChanged(); + void requiresChanged(); + +private: + bool m_providesRecommendations = false; + bool m_recommended = false; + QString m_name; + QString m_uid; + QString m_version; + QString m_type; + qint64 m_time = 0; + Meta::RequireSet m_requires; + Meta::RequireSet m_conflicts; + bool m_volatile = false; + VersionFilePtr m_data; +}; +} + +Q_DECLARE_METATYPE(Meta::VersionPtr) diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp new file mode 100644 index 00000000..607007eb --- /dev/null +++ b/launcher/meta/VersionList.cpp @@ -0,0 +1,245 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VersionList.h" + +#include <QDateTime> + +#include "Version.h" +#include "JsonFormat.h" +#include "Version.h" + +namespace Meta +{ +VersionList::VersionList(const QString &uid, QObject *parent) + : BaseVersionList(parent), m_uid(uid) +{ + setObjectName("Version list: " + uid); +} + +shared_qobject_ptr<Task> VersionList::getLoadTask() +{ + load(Net::Mode::Online); + return getCurrentTask(); +} + +bool VersionList::isLoaded() +{ + return BaseEntity::isLoaded(); +} + +const BaseVersionPtr VersionList::at(int i) const +{ + return m_versions.at(i); +} +int VersionList::count() const +{ + return m_versions.size(); +} + +void VersionList::sortVersions() +{ + beginResetModel(); + std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b) + { + return *a.get() < *b.get(); + }); + endResetModel(); +} + +QVariant VersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid()) + { + return QVariant(); + } + + VersionPtr version = m_versions.at(index.row()); + + switch (role) + { + case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast<BaseVersion>(version)); + case VersionRole: + case VersionIdRole: + return version->version(); + case ParentVersionRole: + { + // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'. + auto & reqs = version->requires(); + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req) + { + return req.uid == "net.minecraft"; + }); + if (iter != reqs.end()) + { + return (*iter).equalsVersion; + } + return QVariant(); + } + case TypeRole: return version->type(); + + case UidRole: return version->uid(); + case TimeRole: return version->time(); + case RequiresRole: return QVariant::fromValue(version->requires()); + case SortRole: return version->rawTime(); + case VersionPtrRole: return QVariant::fromValue(version); + case RecommendedRole: return version->isRecommended(); + // FIXME: this should be determined in whatever view/proxy is used... + // case LatestRole: return version == getLatestStable(); + default: return QVariant(); + } +} + +BaseVersionList::RoleList VersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, + TypeRole, UidRole, TimeRole, RequiresRole, SortRole, + RecommendedRole, LatestRole, VersionPtrRole}; +} + +QHash<int, QByteArray> VersionList::roleNames() const +{ + QHash<int, QByteArray> roles = BaseVersionList::roleNames(); + roles.insert(UidRole, "uid"); + roles.insert(TimeRole, "time"); + roles.insert(SortRole, "sort"); + roles.insert(RequiresRole, "requires"); + return roles; +} + +QString VersionList::localFilename() const +{ + return m_uid + "/index.json"; +} + +QString VersionList::humanReadable() const +{ + return m_name.isEmpty() ? m_uid : m_name; +} + +VersionPtr VersionList::getVersion(const QString &version) +{ + VersionPtr out = m_lookup.value(version, nullptr); + if(!out) + { + out = std::make_shared<Version>(m_uid, version); + m_lookup[version] = out; + } + return out; +} + +void VersionList::setName(const QString &name) +{ + m_name = name; + emit nameChanged(name); +} + +void VersionList::setVersions(const QVector<VersionPtr> &versions) +{ + beginResetModel(); + m_versions = versions; + std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b) + { + return a->rawTime() > b->rawTime(); + }); + for (int i = 0; i < m_versions.size(); ++i) + { + m_lookup.insert(m_versions.at(i)->version(), m_versions.at(i)); + 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(); +} + +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::mergeFromIndex(const VersionListPtr &other) +{ + if (m_name != other->m_name) + { + setName(other->m_name); + } +} + +void VersionList::merge(const VersionListPtr &other) +{ + if (m_name != other->m_name) + { + setName(other->m_name); + } + + // TODO: do not reset the whole model. maybe? + beginResetModel(); + m_versions.clear(); + if(other->m_versions.isEmpty()) + { + qWarning() << "Empty list loaded ..."; + } + for (const VersionPtr &version : other->m_versions) + { + // we already have the version. merge the contents + if (m_lookup.contains(version->version())) + { + m_lookup.value(version->version())->mergeFromList(version); + } + else + { + m_lookup.insert(version->uid(), version); + } + // connect it. + setupAddedVersion(m_versions.size(), version); + m_versions.append(version); + m_recommended = getBetterVersion(m_recommended, version); + } + endResetModel(); +} + +void VersionList::setupAddedVersion(const int row, const VersionPtr &version) +{ + // FIXME: do not disconnect from everythin, disconnect only the lambdas here + version->disconnect(); + connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << RequiresRole); }); + connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TimeRole << SortRole); }); + connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TypeRole); }); +} + +BaseVersionPtr VersionList::getRecommended() const +{ + return m_recommended; +} + +} diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h new file mode 100644 index 00000000..58cdafe7 --- /dev/null +++ b/launcher/meta/VersionList.h @@ -0,0 +1,101 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseEntity.h" +#include "BaseVersionList.h" +#include <QJsonObject> +#include <memory> + +namespace Meta +{ +using VersionPtr = std::shared_ptr<class Version>; +using VersionListPtr = std::shared_ptr<class VersionList>; + +class VersionList : public BaseVersionList, public BaseEntity +{ + Q_OBJECT + Q_PROPERTY(QString uid READ uid CONSTANT) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) +public: + explicit VersionList(const QString &uid, QObject *parent = nullptr); + + enum Roles + { + UidRole = Qt::UserRole + 100, + TimeRole, + RequiresRole, + VersionPtrRole + }; + + shared_qobject_ptr<Task> getLoadTask() override; + bool isLoaded() override; + const BaseVersionPtr at(int i) const override; + int count() const override; + void sortVersions() override; + + BaseVersionPtr getRecommended() const override; + + QVariant data(const QModelIndex &index, int role) const override; + RoleList providesRoles() const override; + QHash<int, QByteArray> roleNames() const override; + + QString localFilename() const override; + + QString uid() const + { + return m_uid; + } + QString name() const + { + return m_name; + } + QString humanReadable() const; + + VersionPtr getVersion(const QString &version); + + QVector<VersionPtr> versions() const + { + return m_versions; + } + +public: // for usage only by parsers + void setName(const QString &name); + void setVersions(const QVector<VersionPtr> &versions); + void merge(const VersionListPtr &other); + void mergeFromIndex(const VersionListPtr &other); + void parse(const QJsonObject &obj) override; + +signals: + void nameChanged(const QString &name); + +protected slots: + void updateListData(QList<BaseVersionPtr>) override + { + } + +private: + QVector<VersionPtr> m_versions; + QHash<QString, VersionPtr> m_lookup; + QString m_uid; + QString m_name; + + VersionPtr m_recommended; + + void setupAddedVersion(const int row, const VersionPtr &version); +}; +} +Q_DECLARE_METATYPE(Meta::VersionListPtr) |