summaryrefslogtreecommitdiff
path: root/meshmc/launcher/meta
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/meta
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/meta')
-rw-r--r--meshmc/launcher/meta/BaseEntity.cpp169
-rw-r--r--meshmc/launcher/meta/BaseEntity.h79
-rw-r--r--meshmc/launcher/meta/Index.cpp162
-rw-r--r--meshmc/launcher/meta/Index.h94
-rw-r--r--meshmc/launcher/meta/Index_test.cpp68
-rw-r--r--meshmc/launcher/meta/JsonFormat.cpp239
-rw-r--r--meshmc/launcher/meta/JsonFormat.h104
-rw-r--r--meshmc/launcher/meta/Version.cpp153
-rw-r--r--meshmc/launcher/meta/Version.h140
-rw-r--r--meshmc/launcher/meta/VersionList.cpp285
-rw-r--r--meshmc/launcher/meta/VersionList.h121
11 files changed, 1614 insertions, 0 deletions
diff --git a/meshmc/launcher/meta/BaseEntity.cpp b/meshmc/launcher/meta/BaseEntity.cpp
new file mode 100644
index 0000000000..144d951e75
--- /dev/null
+++ b/meshmc/launcher/meta/BaseEntity.cpp
@@ -0,0 +1,169 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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 "net/Download.h"
+#include "net/HttpMetaCache.h"
+#include "net/NetJob.h"
+#include "Json.h"
+
+#include "BuildConfig.h"
+#include "Application.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;
+ }
+ m_updateTask =
+ new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()),
+ APPLICATION->network());
+ auto url = this->url();
+ auto entry =
+ APPLICATION->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));
+ m_updateTask->addNetAction(dl);
+ m_updateStatus = UpdateStatus::InProgress;
+ QObject::connect(m_updateTask.get(), &NetJob::succeeded, [this]() {
+ m_loadStatus = LoadStatus::Remote;
+ m_updateStatus = UpdateStatus::Succeeded;
+ });
+ QObject::connect(m_updateTask.get(), &NetJob::failed,
+ [this]() { m_updateStatus = UpdateStatus::Failed; });
+ 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;
+}
+
+Task::Ptr Meta::BaseEntity::getCurrentTask()
+{
+ if (m_updateStatus == UpdateStatus::InProgress) {
+ return m_updateTask;
+ }
+ return nullptr;
+}
diff --git a/meshmc/launcher/meta/BaseEntity.h b/meshmc/launcher/meta/BaseEntity.h
new file mode 100644
index 0000000000..9fd5b9ad19
--- /dev/null
+++ b/meshmc/launcher/meta/BaseEntity.h
@@ -0,0 +1,79 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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"
+#include "net/NetJob.h"
+
+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);
+ Task::Ptr getCurrentTask();
+
+ protected: /* methods */
+ bool loadLocalFile();
+
+ private:
+ LoadStatus m_loadStatus = LoadStatus::NotLoaded;
+ UpdateStatus m_updateStatus = UpdateStatus::NotDone;
+ NetJob::Ptr m_updateTask;
+ };
+} // namespace Meta
diff --git a/meshmc/launcher/meta/Index.cpp b/meshmc/launcher/meta/Index.cpp
new file mode 100644
index 0000000000..ace01c3b84
--- /dev/null
+++ b/meshmc/launcher/meta/Index.cpp
@@ -0,0 +1,162 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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);
+ });
+ }
+} // namespace Meta
diff --git a/meshmc/launcher/meta/Index.h b/meshmc/launcher/meta/Index.h
new file mode 100644
index 0000000000..20809fe15f
--- /dev/null
+++ b/meshmc/launcher/meta/Index.h
@@ -0,0 +1,94 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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);
+ };
+} // namespace Meta
diff --git a/meshmc/launcher/meta/Index_test.cpp b/meshmc/launcher/meta/Index_test.cpp
new file mode 100644
index 0000000000..221ca7c6e6
--- /dev/null
+++ b/meshmc/launcher/meta/Index_test.cpp
@@ -0,0 +1,68 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include "TestUtil.h"
+
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+
+class IndexTest : public QObject
+{
+ Q_OBJECT
+ private slots:
+ 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/meshmc/launcher/meta/JsonFormat.cpp b/meshmc/launcher/meta/JsonFormat.cpp
new file mode 100644
index 0000000000..f624c7d662
--- /dev/null
+++ b/meshmc/launcher/meta/JsonFormat.cpp
@@ -0,0 +1,239 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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 requirements, conflicts;
+ parseRequires(obj, &requirements, "requires");
+ parseRequires(obj, &conflicts, "conflicts");
+ version->setRequires(requirements, 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> requirements;
+ 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);
+ }
+
+} // namespace Meta
diff --git a/meshmc/launcher/meta/JsonFormat.h b/meshmc/launcher/meta/JsonFormat.h
new file mode 100644
index 0000000000..933c2391c6
--- /dev/null
+++ b/meshmc/launcher/meta/JsonFormat.h
@@ -0,0 +1,104 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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();
+} // namespace Meta
+
+Q_DECLARE_METATYPE(std::set<Meta::Require>)
diff --git a/meshmc/launcher/meta/Version.cpp b/meshmc/launcher/meta/Version.cpp
new file mode 100644
index 0000000000..f09f5d0a8c
--- /dev/null
+++ b/meshmc/launcher/meta/Version.cpp
@@ -0,0 +1,153 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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& requirements,
+ const Meta::RequireSet& conflicts)
+{
+ m_requires = requirements;
+ 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/meshmc/launcher/meta/Version.h b/meshmc/launcher/meta/Version.h
new file mode 100644
index 0000000000..04caedc1c2
--- /dev/null
+++ b/meshmc/launcher/meta/Version.h
@@ -0,0 +1,140 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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& requirements() 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& requirements,
+ 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;
+ };
+} // namespace Meta
+
+Q_DECLARE_METATYPE(Meta::VersionPtr)
diff --git a/meshmc/launcher/meta/VersionList.cpp b/meshmc/launcher/meta/VersionList.cpp
new file mode 100644
index 0000000000..1bb150616a
--- /dev/null
+++ b/meshmc/launcher/meta/VersionList.cpp
@@ -0,0 +1,285 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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);
+ }
+
+ Task::Ptr 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->requirements();
+ 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->requirements());
+ 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;
+ }
+
+} // namespace Meta
diff --git a/meshmc/launcher/meta/VersionList.h b/meshmc/launcher/meta/VersionList.h
new file mode 100644
index 0000000000..da903b18e2
--- /dev/null
+++ b/meshmc/launcher/meta/VersionList.h
@@ -0,0 +1,121 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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
+ };
+
+ Task::Ptr 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);
+ };
+} // namespace Meta
+Q_DECLARE_METATYPE(Meta::VersionListPtr)