/* 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 .
*
* 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
#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(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 VersionList::roleNames() const
{
QHash 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(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& 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() << RequiresRole);
});
connect(version.get(), &Version::timeChanged, this, [this, row]() {
emit dataChanged(index(row), index(row),
QVector() << TimeRole << SortRole);
});
connect(version.get(), &Version::typeChanged, this, [this, row]() {
emit dataChanged(index(row), index(row),
QVector() << TypeRole);
});
}
BaseVersionPtr VersionList::getRecommended() const
{
return m_recommended;
}
} // namespace Meta