summaryrefslogtreecommitdiffstats
path: root/logic/lists
diff options
context:
space:
mode:
Diffstat (limited to 'logic/lists')
-rw-r--r--logic/lists/BaseVersionList.cpp121
-rw-r--r--logic/lists/BaseVersionList.h120
-rw-r--r--logic/lists/ForgeVersionList.cpp439
-rw-r--r--logic/lists/ForgeVersionList.h128
-rw-r--r--logic/lists/InstanceList.cpp608
-rw-r--r--logic/lists/InstanceList.h139
-rw-r--r--logic/lists/JavaVersionList.cpp242
-rw-r--r--logic/lists/JavaVersionList.h96
-rw-r--r--logic/lists/LwjglVersionList.cpp199
-rw-r--r--logic/lists/LwjglVersionList.h148
-rw-r--r--logic/lists/MinecraftVersionList.cpp286
-rw-r--r--logic/lists/MinecraftVersionList.h74
12 files changed, 2600 insertions, 0 deletions
diff --git a/logic/lists/BaseVersionList.cpp b/logic/lists/BaseVersionList.cpp
new file mode 100644
index 00000000..6e2c5282
--- /dev/null
+++ b/logic/lists/BaseVersionList.cpp
@@ -0,0 +1,121 @@
+/* Copyright 2013 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 "logic/lists/BaseVersionList.h"
+#include "logic/BaseVersion.h"
+
+BaseVersionList::BaseVersionList(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+BaseVersionPtr BaseVersionList::findVersion(const QString &descriptor)
+{
+ for (int i = 0; i < count(); i++)
+ {
+ if (at(i)->descriptor() == descriptor)
+ return at(i);
+ }
+ return BaseVersionPtr();
+}
+
+BaseVersionPtr BaseVersionList::getLatestStable() const
+{
+ if (count() <= 0)
+ return BaseVersionPtr();
+ else
+ return at(0);
+}
+
+QVariant BaseVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ BaseVersionPtr version = at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case NameColumn:
+ return version->name();
+
+ case TypeColumn:
+ return version->typeString();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return version->descriptor();
+
+ case VersionPointerRole:
+ return qVariantFromValue(version);
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant BaseVersionList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case NameColumn:
+ return "Name";
+
+ case TypeColumn:
+ return "Type";
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case NameColumn:
+ return "The name of the version.";
+
+ case TypeColumn:
+ return "The version's type.";
+
+ default:
+ return QVariant();
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+int BaseVersionList::rowCount(const QModelIndex &parent) const
+{
+ // Return count
+ return count();
+}
+
+int BaseVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 2;
+}
diff --git a/logic/lists/BaseVersionList.h b/logic/lists/BaseVersionList.h
new file mode 100644
index 00000000..21b44e8d
--- /dev/null
+++ b/logic/lists/BaseVersionList.h
@@ -0,0 +1,120 @@
+/* Copyright 2013 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 <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+
+#include "logic/BaseVersion.h"
+
+class Task;
+
+/*!
+ * \brief Class that each instance type's version list derives from.
+ * Version lists are the lists that keep track of the available game versions
+ * for that instance. This list will not be loaded on startup. It will be loaded
+ * when the list's load function is called. Before using the version list, you
+ * should check to see if it has been loaded yet and if not, load the list.
+ *
+ * Note that this class also inherits from QAbstractListModel. Methods from that
+ * class determine how this version list shows up in a list view. Said methods
+ * all have a default implementation, but they can be overridden by plugins to
+ * change the behavior of the list.
+ */
+class BaseVersionList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ModelRoles
+ {
+ VersionPointerRole = 0x34B1CB48
+ };
+
+ enum VListColumns
+ {
+ // First column - Name
+ NameColumn = 0,
+
+ // Second column - Type
+ TypeColumn,
+
+ // Third column - Timestamp
+ TimeColumn
+ };
+
+ explicit BaseVersionList(QObject *parent = 0);
+
+ /*!
+ * \brief Gets a task that will reload the version list.
+ * Simply execute the task to load the list.
+ * The task returned by this function should reset the model when it's done.
+ * \return A pointer to a task that reloads the version list.
+ */
+ virtual Task *getLoadTask() = 0;
+
+ //! Checks whether or not the list is loaded. If this returns false, the list should be
+ //loaded.
+ virtual bool isLoaded() = 0;
+
+ //! Gets the version at the given index.
+ virtual const BaseVersionPtr at(int i) const = 0;
+
+ //! Returns the number of versions in the list.
+ virtual int count() const = 0;
+
+ //////// List Model Functions ////////
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ virtual int rowCount(const QModelIndex &parent) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+
+ /*!
+ * \brief Finds a version by its descriptor.
+ * \param The descriptor of the version to find.
+ * \return A const pointer to the version with the given descriptor. NULL if
+ * one doesn't exist.
+ */
+ virtual BaseVersionPtr findVersion(const QString &descriptor);
+
+ /*!
+ * \brief Gets the latest stable version of this instance type.
+ * This is the version that will be selected by default.
+ * By default, this is simply the first version in the list.
+ */
+ virtual BaseVersionPtr getLatestStable() const;
+
+ /*!
+ * Sorts the version list.
+ */
+ virtual void sort() = 0;
+
+protected
+slots:
+ /*!
+ * Updates this list with the given list of versions.
+ * This is done by copying each version in the given list and inserting it
+ * into this one.
+ * We need to do this so that we can set the parents of the versions are set to this
+ * version list. This can't be done in the load task, because the versions the load
+ * task creates are on the load task's thread and Qt won't allow their parents
+ * to be set to something created on another thread.
+ * To get around that problem, we invoke this method on the GUI thread, which
+ * then copies the versions and sets their parents correctly.
+ * \param versions List of versions whose parents should be set.
+ */
+ virtual void updateListData(QList<BaseVersionPtr> versions) = 0;
+};
diff --git a/logic/lists/ForgeVersionList.cpp b/logic/lists/ForgeVersionList.cpp
new file mode 100644
index 00000000..4902dc64
--- /dev/null
+++ b/logic/lists/ForgeVersionList.cpp
@@ -0,0 +1,439 @@
+/* Copyright 2013 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 "ForgeVersionList.h"
+#include <logic/net/NetJob.h>
+#include <logic/net/URLConstants.h>
+#include "MultiMC.h"
+
+#include <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include "logger/QsLog.h"
+
+ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *ForgeVersionList::getLoadTask()
+{
+ return new ForgeListLoadTask(this);
+}
+
+bool ForgeVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr ForgeVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int ForgeVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+int ForgeVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 3;
+}
+
+QVariant ForgeVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<ForgeVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case 0:
+ return version->name();
+
+ case 1:
+ return version->mcver;
+
+ case 2:
+ return version->typeString();
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return version->descriptor();
+
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant ForgeVersionList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case 0:
+ return "Version";
+
+ case 1:
+ return "Minecraft";
+
+ case 2:
+ return "Type";
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case 0:
+ return "The name of the version.";
+
+ case 1:
+ return "Minecraft version";
+
+ case 2:
+ return "The version's type.";
+
+ default:
+ return QVariant();
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionPtr ForgeVersionList::getLatestStable() const
+{
+ return BaseVersionPtr();
+}
+
+void ForgeVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ endResetModel();
+ // NOW SORT!!
+ // sort();
+}
+
+void ForgeVersionList::sort()
+{
+ // NO-OP for now
+}
+
+ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task()
+{
+ m_list = vlist;
+}
+
+void ForgeListLoadTask::executeTask()
+{
+ setStatus(tr("Fetching Forge version lists..."));
+ auto job = new NetJob("Version index");
+ // we do not care if the version is stale or not.
+ auto forgeListEntry = MMC->metacache()->resolveEntry("minecraftforge", "list.json");
+ auto gradleForgeListEntry = MMC->metacache()->resolveEntry("minecraftforge", "json");
+
+ // verify by poking the server.
+ forgeListEntry->stale = true;
+ gradleForgeListEntry->stale = true;
+
+ job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL),
+ forgeListEntry));
+ job->addNetAction(gradleListDownload = CacheDownload::make(
+ QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry));
+
+ connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
+ connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed()));
+
+ listJob.reset(job);
+ connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
+ connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ listJob->start();
+}
+
+bool ForgeListLoadTask::parseForgeList(QList<BaseVersionPtr> &out)
+{
+ QByteArray data;
+ {
+ auto dlJob = listDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ data = listFile.readAll();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing version list JSON:" + jsonError.errorString());
+ return false;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing version list JSON: JSON root is not an object");
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // Now, get the array of versions.
+ if (!root.value("builds").isArray())
+ {
+ emitFailed(
+ "Error parsing version list JSON: version list object is missing 'builds' array");
+ return false;
+ }
+ QJsonArray builds = root.value("builds").toArray();
+
+ for (int i = 0; i < builds.count(); i++)
+ {
+ // Load the version info.
+ if (!builds[i].isObject())
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+ QJsonObject obj = builds[i].toObject();
+ int build_nr = obj.value("build").toDouble(0);
+ if (!build_nr)
+ continue;
+ QJsonArray files = obj.value("files").toArray();
+ QString url, jobbuildver, mcver, buildtype, filename;
+ QString changelog_url, installer_url;
+ QString installer_filename;
+ bool valid = false;
+ for (int j = 0; j < files.count(); j++)
+ {
+ if (!files[j].isObject())
+ {
+ continue;
+ }
+ QJsonObject file = files[j].toObject();
+ buildtype = file.value("buildtype").toString();
+ if ((buildtype == "client" || buildtype == "universal") && !valid)
+ {
+ mcver = file.value("mcver").toString();
+ url = file.value("url").toString();
+ jobbuildver = file.value("jobbuildver").toString();
+ int lastSlash = url.lastIndexOf('/');
+ filename = url.mid(lastSlash + 1);
+ valid = true;
+ }
+ else if (buildtype == "changelog")
+ {
+ QString ext = file.value("ext").toString();
+ if (ext.isEmpty())
+ {
+ continue;
+ }
+ changelog_url = file.value("url").toString();
+ }
+ else if (buildtype == "installer")
+ {
+ installer_url = file.value("url").toString();
+ int lastSlash = installer_url.lastIndexOf('/');
+ installer_filename = installer_url.mid(lastSlash + 1);
+ }
+ }
+ if (valid)
+ {
+ // Now, we construct the version object and add it to the list.
+ std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
+ fVersion->universal_url = url;
+ fVersion->changelog_url = changelog_url;
+ fVersion->installer_url = installer_url;
+ fVersion->jobbuildver = jobbuildver;
+ fVersion->mcver = mcver;
+ if (installer_filename.isEmpty())
+ {
+ fVersion->filename = filename;
+ }
+ else
+ {
+ fVersion->filename = installer_filename;
+ }
+ fVersion->m_buildnr = build_nr;
+ out.append(fVersion);
+ }
+ }
+
+ return true;
+}
+
+bool ForgeListLoadTask::parseForgeGradleList(QList<BaseVersionPtr> &out)
+{
+ QByteArray data;
+ {
+ auto dlJob = gradleListDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ data = listFile.readAll();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString());
+ return false;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing gradle version list JSON: JSON root is not an object");
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // we probably could hard code these, but it might still be worth doing it this way
+ const QString webpath = root.value("webpath").toString();
+ const QString artifact = root.value("artifact").toString();
+
+ QJsonObject numbers = root.value("number").toObject();
+ for (auto it = numbers.begin(); it != numbers.end(); ++it)
+ {
+ QJsonObject number = it.value().toObject();
+ std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
+ fVersion->m_buildnr = number.value("build").toDouble();
+ fVersion->jobbuildver = number.value("version").toString();
+ fVersion->mcver = number.value("mcversion").toString();
+ fVersion->filename = "";
+ QString filename, installer_filename;
+ QJsonArray files = number.value("files").toArray();
+ for (auto fIt = files.begin(); fIt != files.end(); ++fIt)
+ {
+ // TODO with gradle we also get checksums, use them
+ QJsonArray file = (*fIt).toArray();
+ if (file.size() < 3)
+ {
+ continue;
+ }
+ if (file.at(1).toString() == "installer")
+ {
+ fVersion->installer_url = QString("%1/%2-%3/%4-%2-%3-installer.%5").arg(
+ webpath, fVersion->mcver, fVersion->jobbuildver, artifact,
+ file.at(0).toString());
+ installer_filename = QString("%1-%2-%3-installer.%4").arg(
+ artifact, fVersion->mcver, fVersion->jobbuildver, file.at(0).toString());
+ }
+ else if (file.at(1).toString() == "universal")
+ {
+ fVersion->universal_url = QString("%1/%2-%3/%4-%2-%3-universal.%5").arg(
+ webpath, fVersion->mcver, fVersion->jobbuildver, artifact,
+ file.at(0).toString());
+ filename = QString("%1-%2-%3-universal.%4").arg(
+ artifact, fVersion->mcver, fVersion->jobbuildver, file.at(0).toString());
+ }
+ else if (file.at(1).toString() == "changelog")
+ {
+ fVersion->changelog_url = QString("%1/%2-%3/%4-%2-%3-changelog.%5").arg(
+ webpath, fVersion->mcver, fVersion->jobbuildver, artifact,
+ file.at(0).toString());
+ }
+ }
+ if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty())
+ {
+ continue;
+ }
+ fVersion->filename = fVersion->installer_url.isEmpty() ? filename : installer_filename;
+ out.append(fVersion);
+ }
+
+ return true;
+}
+
+void ForgeListLoadTask::listDownloaded()
+{
+ QList<BaseVersionPtr> list;
+ bool ret = true;
+ if (!parseForgeList(list))
+ {
+ ret = false;
+ }
+ if (!parseForgeGradleList(list))
+ {
+ ret = false;
+ }
+
+ if (!ret)
+ {
+ return;
+ }
+ std::sort(list.begin(), list.end(), [](const BaseVersionPtr & l, const BaseVersionPtr & r)
+ { return (*l > *r); });
+
+ m_list->updateListData(list);
+
+ emitSucceeded();
+ return;
+}
+
+void ForgeListLoadTask::listFailed()
+{
+ auto reply = listDownload->m_reply;
+ if (reply)
+ {
+ QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString();
+ }
+ else
+ {
+ QLOG_ERROR() << "Getting forge version list failed for reasons unknown.";
+ }
+}
+void ForgeListLoadTask::gradleListFailed()
+{
+ auto reply = gradleListDownload->m_reply;
+ if (reply)
+ {
+ QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString();
+ }
+ else
+ {
+ QLOG_ERROR() << "Getting forge version list failed for reasons unknown.";
+ }
+}
diff --git a/logic/lists/ForgeVersionList.h b/logic/lists/ForgeVersionList.h
new file mode 100644
index 00000000..b19d3f56
--- /dev/null
+++ b/logic/lists/ForgeVersionList.h
@@ -0,0 +1,128 @@
+/* Copyright 2013 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 <QObject>
+#include <QAbstractListModel>
+#include <QUrl>
+
+#include <QNetworkReply>
+#include "BaseVersionList.h"
+#include "logic/tasks/Task.h"
+#include "logic/net/NetJob.h"
+
+class ForgeVersion;
+typedef std::shared_ptr<ForgeVersion> ForgeVersionPtr;
+
+struct ForgeVersion : public BaseVersion
+{
+ virtual QString descriptor() override
+ {
+ return filename;
+ }
+ ;
+ virtual QString name() override
+ {
+ return "Forge " + jobbuildver;
+ }
+ ;
+ virtual QString typeString() const override
+ {
+ if (installer_url.isEmpty())
+ return "Universal";
+ else
+ return "Installer";
+ }
+
+ virtual bool operator<(BaseVersion &a) override
+ {
+ ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
+ if(!pa)
+ return true;
+ return m_buildnr < pa->m_buildnr;
+ }
+ virtual bool operator>(BaseVersion &a) override
+ {
+ ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
+ if(!pa)
+ return false;
+ return m_buildnr > pa->m_buildnr;
+ }
+ int m_buildnr = 0;
+ QString universal_url;
+ QString changelog_url;
+ QString installer_url;
+ QString jobbuildver;
+ QString mcver;
+ QString filename;
+};
+
+class ForgeVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ friend class ForgeListLoadTask;
+
+ explicit ForgeVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask();
+ virtual bool isLoaded();
+ virtual const BaseVersionPtr at(int i) const;
+ virtual int count() const;
+ virtual void sort();
+
+ virtual BaseVersionPtr getLatestStable() const;
+
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions);
+};
+
+class ForgeListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit ForgeListLoadTask(ForgeVersionList *vlist);
+
+ virtual void executeTask();
+
+protected
+slots:
+ void listDownloaded();
+ void listFailed();
+ void gradleListFailed();
+
+protected:
+ NetJobPtr listJob;
+ ForgeVersionList *m_list;
+
+ CacheDownloadPtr listDownload;
+ CacheDownloadPtr gradleListDownload;
+
+private:
+ bool parseForgeList(QList<BaseVersionPtr> &out);
+ bool parseForgeGradleList(QList<BaseVersionPtr> &out);
+};
diff --git a/logic/lists/InstanceList.cpp b/logic/lists/InstanceList.cpp
new file mode 100644
index 00000000..0d4eab95
--- /dev/null
+++ b/logic/lists/InstanceList.cpp
@@ -0,0 +1,608 @@
+/* Copyright 2013 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 <QDir>
+#include <QSet>
+#include <QFile>
+#include <QDirIterator>
+#include <QThread>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QXmlStreamReader>
+#include <QRegularExpression>
+#include <pathutils.h>
+
+#include "MultiMC.h"
+#include "logic/lists/InstanceList.h"
+#include "logic/icons/IconList.h"
+#include "logic/lists/MinecraftVersionList.h"
+#include "logic/BaseInstance.h"
+#include "logic/InstanceFactory.h"
+#include "logger/QsLog.h"
+
+const static int GROUP_FILE_FORMAT_VERSION = 1;
+
+InstanceList::InstanceList(const QString &instDir, QObject *parent)
+ : QAbstractListModel(parent), m_instDir(instDir)
+{
+ connect(MMC, &MultiMC::aboutToQuit, this, &InstanceList::saveGroupList);
+
+ if (!QDir::current().exists(m_instDir))
+ {
+ QDir::current().mkpath(m_instDir);
+ }
+
+ connect(MMC->minecraftlist().get(), &MinecraftVersionList::modelReset, this,
+ &InstanceList::loadList);
+}
+
+InstanceList::~InstanceList()
+{
+}
+
+int InstanceList::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return m_instances.count();
+}
+
+QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ if (row < 0 || row >= m_instances.size())
+ return QModelIndex();
+ return createIndex(row, column, (void *)m_instances.at(row).get());
+}
+
+QVariant InstanceList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ {
+ return QVariant();
+ }
+ BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
+ switch (role)
+ {
+ case InstancePointerRole:
+ {
+ QVariant v = qVariantFromValue((void *)pdata);
+ return v;
+ }
+ case Qt::DisplayRole:
+ {
+ return pdata->name();
+ }
+ case Qt::ToolTipRole:
+ {
+ return pdata->instanceRoot();
+ }
+ case Qt::DecorationRole:
+ {
+ QString key = pdata->iconKey();
+ return MMC->icons()->getIcon(key);
+ }
+ // for now.
+ case KCategorizedSortFilterProxyModel::CategorySortRole:
+ case KCategorizedSortFilterProxyModel::CategoryDisplayRole:
+ {
+ return pdata->group();
+ }
+ default:
+ break;
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags f;
+ if (index.isValid())
+ {
+ f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ }
+ return f;
+}
+
+void InstanceList::groupChanged()
+{
+ // save the groups. save all of them.
+ saveGroupList();
+}
+
+QStringList InstanceList::getGroups()
+{
+ return m_groups.toList();
+}
+
+void InstanceList::saveGroupList()
+{
+ QString groupFileName = m_instDir + "/instgroups.json";
+ QFile groupFile(groupFileName);
+
+ // if you can't open the file, fail
+ if (!groupFile.open(QIODevice::WriteOnly | QIODevice::Truncate))
+ {
+ // An error occurred. Ignore it.
+ QLOG_ERROR() << "Failed to save instance group file.";
+ return;
+ }
+ QTextStream out(&groupFile);
+ QMap<QString, QSet<QString>> groupMap;
+ for (auto instance : m_instances)
+ {
+ QString id = instance->id();
+ QString group = instance->group();
+ if (group.isEmpty())
+ continue;
+
+ // keep a list/set of groups for choosing
+ m_groups.insert(group);
+
+ if (!groupMap.count(group))
+ {
+ QSet<QString> set;
+ set.insert(id);
+ groupMap[group] = set;
+ }
+ else
+ {
+ QSet<QString> &set = groupMap[group];
+ set.insert(id);
+ }
+ }
+ QJsonObject toplevel;
+ toplevel.insert("formatVersion", QJsonValue(QString("1")));
+ QJsonObject groupsArr;
+ for (auto iter = groupMap.begin(); iter != groupMap.end(); iter++)
+ {
+ auto list = iter.value();
+ auto name = iter.key();
+ QJsonObject groupObj;
+ QJsonArray instanceArr;
+ groupObj.insert("hidden", QJsonValue(QString("false")));
+ for (auto item : list)
+ {
+ instanceArr.append(QJsonValue(item));
+ }
+ groupObj.insert("instances", instanceArr);
+ groupsArr.insert(name, groupObj);
+ }
+ toplevel.insert("groups", groupsArr);
+ QJsonDocument doc(toplevel);
+ groupFile.write(doc.toJson());
+ groupFile.close();
+}
+
+void InstanceList::loadGroupList(QMap<QString, QString> &groupMap)
+{
+ QString groupFileName = m_instDir + "/instgroups.json";
+
+ // if there's no group file, fail
+ if (!QFileInfo(groupFileName).exists())
+ return;
+
+ QFile groupFile(groupFileName);
+
+ // if you can't open the file, fail
+ if (!groupFile.open(QIODevice::ReadOnly))
+ {
+ // An error occurred. Ignore it.
+ QLOG_ERROR() << "Failed to read instance group file.";
+ return;
+ }
+
+ QTextStream in(&groupFile);
+ QString jsonStr = in.readAll();
+ groupFile.close();
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonStr.toUtf8(), &error);
+
+ // if the json was bad, fail
+ if (error.error != QJsonParseError::NoError)
+ {
+ QLOG_ERROR() << QString("Failed to parse instance group file: %1 at offset %2")
+ .arg(error.errorString(), QString::number(error.offset))
+ .toUtf8();
+ return;
+ }
+
+ // if the root of the json wasn't an object, fail
+ if (!jsonDoc.isObject())
+ {
+ QLOG_WARN() << "Invalid group file. Root entry should be an object.";
+ return;
+ }
+
+ QJsonObject rootObj = jsonDoc.object();
+
+ // Make sure the format version matches, otherwise fail.
+ if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION)
+ return;
+
+ // Get the groups. if it's not an object, fail
+ if (!rootObj.value("groups").isObject())
+ {
+ QLOG_WARN() << "Invalid group list JSON: 'groups' should be an object.";
+ return;
+ }
+
+ // Iterate through all the groups.
+ QJsonObject groupMapping = rootObj.value("groups").toObject();
+ for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++)
+ {
+ QString groupName = iter.key();
+
+ // If not an object, complain and skip to the next one.
+ if (!iter.value().isObject())
+ {
+ QLOG_WARN() << QString("Group '%1' in the group list should "
+ "be an object.")
+ .arg(groupName)
+ .toUtf8();
+ continue;
+ }
+
+ QJsonObject groupObj = iter.value().toObject();
+ if (!groupObj.value("instances").isArray())
+ {
+ QLOG_WARN() << QString("Group '%1' in the group list is invalid. "
+ "It should contain an array "
+ "called 'instances'.")
+ .arg(groupName)
+ .toUtf8();
+ continue;
+ }
+
+ // keep a list/set of groups for choosing
+ m_groups.insert(groupName);
+
+ // Iterate through the list of instances in the group.
+ QJsonArray instancesArray = groupObj.value("instances").toArray();
+
+ for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end();
+ iter2++)
+ {
+ groupMap[(*iter2).toString()] = groupName;
+ }
+ }
+}
+
+struct FTBRecord
+{
+ QString dir;
+ QString name;
+ QString logo;
+ QString mcVersion;
+ QString description;
+};
+
+void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap)
+{
+ QList<FTBRecord> records;
+ QDir dir = QDir(MMC->settings()->get("FTBLauncherRoot").toString());
+ QDir dataDir = QDir(MMC->settings()->get("FTBRoot").toString());
+ if (!dir.exists())
+ {
+ QLOG_INFO() << "The FTB launcher directory specified does not exist. Please check your "
+ "settings.";
+ return;
+ }
+ else if (!dataDir.exists())
+ {
+ QLOG_INFO() << "The FTB directory specified does not exist. Please check your settings";
+ return;
+ }
+ dir.cd("ModPacks");
+ auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name);
+ for(auto filename: allFiles)
+ {
+ if(!filename.endsWith(".xml"))
+ continue;
+ auto fpath = dir.absoluteFilePath(filename);
+ QFile f(fpath);
+ QLOG_INFO() << "Discovering FTB instances -- " << fpath;
+ if (!f.open(QFile::ReadOnly))
+ continue;
+
+ // read the FTB packs XML.
+ QXmlStreamReader reader(&f);
+ while (!reader.atEnd())
+ {
+ switch (reader.readNext())
+ {
+ case QXmlStreamReader::StartElement:
+ {
+ if (reader.name() == "modpack")
+ {
+ QXmlStreamAttributes attrs = reader.attributes();
+ FTBRecord record;
+ record.dir = attrs.value("dir").toString();
+ QDir test(dataDir.absoluteFilePath(record.dir));
+ if(!test.exists())
+ continue;
+ record.name = attrs.value("name").toString();
+ record.logo = attrs.value("logo").toString();
+ record.mcVersion = attrs.value("mcVersion").toString();
+ record.description = attrs.value("description").toString();
+ records.append(record);
+ }
+ break;
+ }
+ case QXmlStreamReader::EndElement:
+ break;
+ case QXmlStreamReader::Characters:
+ break;
+ default:
+ break;
+ }
+ }
+ f.close();
+ }
+
+ if(!records.size())
+ {
+ QLOG_INFO() << "No FTB instances to load.";
+ return;
+ }
+ QLOG_INFO() << "Loading FTB instances! -- got " << records.size();
+ // process the records we acquired.
+ for (auto record : records)
+ {
+ auto instanceDir = dataDir.absoluteFilePath(record.dir);
+ QLOG_INFO() << "Loading FTB instance from " << instanceDir;
+ auto templateDir = dir.absoluteFilePath(record.dir);
+ if (!QFileInfo(instanceDir).exists())
+ {
+ continue;
+ }
+
+ QString iconKey = record.logo;
+ iconKey.remove(QRegularExpression("\\..*"));
+ MMC->icons()->addIcon(iconKey, iconKey, PathCombine(templateDir, record.logo),
+ MMCIcon::Transient);
+
+ if (!QFileInfo(PathCombine(instanceDir, "instance.cfg")).exists())
+ {
+ QLOG_INFO() << "Converting " << record.name << " as new.";
+ BaseInstance *instPtr = NULL;
+ auto &factory = InstanceFactory::get();
+ auto version = MMC->minecraftlist()->findVersion(record.mcVersion);
+ if (!version)
+ {
+ QLOG_ERROR() << "Can't load instance " << instanceDir
+ << " because minecraft version " << record.mcVersion
+ << " can't be resolved.";
+ continue;
+ }
+ auto error = factory.createInstance(instPtr, version, instanceDir,
+ InstanceFactory::FTBInstance);
+
+ if (!instPtr || error != InstanceFactory::NoCreateError)
+ continue;
+
+ instPtr->setGroupInitial("FTB");
+ instPtr->setName(record.name);
+ instPtr->setIconKey(iconKey);
+ instPtr->setIntendedVersionId(record.mcVersion);
+ instPtr->setNotes(record.description);
+ continueProcessInstance(instPtr, error, instanceDir, groupMap);
+ }
+ else
+ {
+ QLOG_INFO() << "Loading existing " << record.name;
+ BaseInstance *instPtr = NULL;
+ auto error = InstanceFactory::get().loadInstance(instPtr, instanceDir);
+ if (!instPtr || error != InstanceFactory::NoCreateError)
+ continue;
+ instPtr->setGroupInitial("FTB");
+ instPtr->setName(record.name);
+ instPtr->setIconKey(iconKey);
+ if (instPtr->intendedVersionId() != record.mcVersion)
+ instPtr->setIntendedVersionId(record.mcVersion);
+ instPtr->setNotes(record.description);
+ continueProcessInstance(instPtr, error, instanceDir, groupMap);
+ }
+ }
+}
+
+InstanceList::InstListError InstanceList::loadList()
+{
+ // load the instance groups
+ QMap<QString, QString> groupMap;
+ loadGroupList(groupMap);
+
+ beginResetModel();
+
+ m_instances.clear();
+
+ {
+ QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable,
+ QDirIterator::FollowSymlinks);
+ while (iter.hasNext())
+ {
+ QString subDir = iter.next();
+ if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists())
+ continue;
+ QLOG_INFO() << "Loading MultiMC instance from " << subDir;
+ BaseInstance *instPtr = NULL;
+ auto error = InstanceFactory::get().loadInstance(instPtr, subDir);
+ continueProcessInstance(instPtr, error, subDir, groupMap);
+ }
+ }
+
+ if (MMC->settings()->get("TrackFTBInstances").toBool())
+ {
+ loadForgeInstances(groupMap);
+ }
+
+ endResetModel();
+ emit dataIsInvalid();
+ return NoError;
+}
+
+/// Clear all instances. Triggers notifications.
+void InstanceList::clear()
+{
+ beginResetModel();
+ saveGroupList();
+ m_instances.clear();
+ endResetModel();
+ emit dataIsInvalid();
+}
+
+void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
+{
+ m_instDir = value.toString();
+ loadList();
+}
+
+/// Add an instance. Triggers notifications, returns the new index
+int InstanceList::add(InstancePtr t)
+{
+ beginInsertRows(QModelIndex(), m_instances.size(), m_instances.size());
+ m_instances.append(t);
+ t->setParent(this);
+ connect(t.get(), SIGNAL(propertiesChanged(BaseInstance *)), this,
+ SLOT(propertiesChanged(BaseInstance *)));
+ connect(t.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged()));
+ connect(t.get(), SIGNAL(nuked(BaseInstance *)), this, SLOT(instanceNuked(BaseInstance *)));
+ endInsertRows();
+ return count() - 1;
+}
+
+InstancePtr InstanceList::getInstanceById(QString instId) const
+{
+ if (m_instances.isEmpty())
+ {
+ return InstancePtr();
+ }
+
+ QListIterator<InstancePtr> iter(m_instances);
+ InstancePtr inst;
+ while (iter.hasNext())
+ {
+ inst = iter.next();
+ if (inst->id() == instId)
+ break;
+ }
+ if (inst->id() != instId)
+ return InstancePtr();
+ else
+ return iter.peekPrevious();
+}
+
+QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
+{
+ return index(getInstIndex(getInstanceById(id).get()));
+}
+
+int InstanceList::getInstIndex(BaseInstance *inst) const
+{
+ for (int i = 0; i < m_instances.count(); i++)
+ {
+ if (inst == m_instances[i].get())
+ {
+ return i;
+ }
+ }
+ return -1;
+}
+
+void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int error,
+ const QDir &dir, QMap<QString, QString> &groupMap)
+{
+ if (error != InstanceFactory::NoLoadError && error != InstanceFactory::NotAnInstance)
+ {
+ QString errorMsg = QString("Failed to load instance %1: ")
+ .arg(QFileInfo(dir.absolutePath()).baseName())
+ .toUtf8();
+
+ switch (error)
+ {
+ default:
+ errorMsg += QString("Unknown instance loader error %1").arg(error);
+ break;
+ }
+ QLOG_ERROR() << errorMsg.toUtf8();
+ }
+ else if (!instPtr)
+ {
+ QLOG_ERROR() << QString("Error loading instance %1. Instance loader returned null.")
+ .arg(QFileInfo(dir.absolutePath()).baseName())
+ .toUtf8();
+ }
+ else
+ {
+ auto iter = groupMap.find(instPtr->id());
+ if (iter != groupMap.end())
+ {
+ instPtr->setGroupInitial((*iter));
+ }
+ QLOG_INFO() << "Loaded instance " << instPtr->name() << " from " << dir.absolutePath();
+ instPtr->setParent(this);
+ m_instances.append(std::shared_ptr<BaseInstance>(instPtr));
+ connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this,
+ SLOT(propertiesChanged(BaseInstance *)));
+ connect(instPtr, SIGNAL(groupChanged()), this, SLOT(groupChanged()));
+ connect(instPtr, SIGNAL(nuked(BaseInstance *)), this,
+ SLOT(instanceNuked(BaseInstance *)));
+ }
+}
+
+void InstanceList::instanceNuked(BaseInstance *inst)
+{
+ int i = getInstIndex(inst);
+ if (i != -1)
+ {
+ beginRemoveRows(QModelIndex(), i, i);
+ m_instances.removeAt(i);
+ endRemoveRows();
+ }
+}
+
+void InstanceList::propertiesChanged(BaseInstance *inst)
+{
+ int i = getInstIndex(inst);
+ if (i != -1)
+ {
+ emit dataChanged(index(i), index(i));
+ }
+}
+
+InstanceProxyModel::InstanceProxyModel(QObject *parent)
+ : KCategorizedSortFilterProxyModel(parent)
+{
+ // disable since by default we are globally sorting by date:
+ setCategorizedModel(true);
+}
+
+bool InstanceProxyModel::subSortLessThan(const QModelIndex &left,
+ const QModelIndex &right) const
+{
+ BaseInstance *pdataLeft = static_cast<BaseInstance *>(left.internalPointer());
+ BaseInstance *pdataRight = static_cast<BaseInstance *>(right.internalPointer());
+ QString sortMode = MMC->settings()->get("InstSortMode").toString();
+ if (sortMode == "LastLaunch")
+ {
+ return pdataLeft->lastLaunch() > pdataRight->lastLaunch();
+ }
+ else
+ {
+ return QString::localeAwareCompare(pdataLeft->name(), pdataRight->name()) < 0;
+ }
+}
diff --git a/logic/lists/InstanceList.h b/logic/lists/InstanceList.h
new file mode 100644
index 00000000..0ce808e5
--- /dev/null
+++ b/logic/lists/InstanceList.h
@@ -0,0 +1,139 @@
+/* Copyright 2013 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 <QObject>
+#include <QAbstractListModel>
+#include <QSet>
+#include "categorizedsortfilterproxymodel.h"
+#include <QIcon>
+
+#include "logic/BaseInstance.h"
+
+class BaseInstance;
+
+class QDir;
+
+class InstanceList : public QAbstractListModel
+{
+ Q_OBJECT
+private:
+ void loadGroupList(QMap<QString, QString> &groupList);
+
+private
+slots:
+ void saveGroupList();
+
+public:
+ explicit InstanceList(const QString &instDir, QObject *parent = 0);
+ virtual ~InstanceList();
+
+public:
+ QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+
+ enum AdditionalRoles
+ {
+ InstancePointerRole = 0x34B1CB48 ///< Return pointer to real instance
+ };
+ /*!
+ * \brief Error codes returned by functions in the InstanceList class.
+ * NoError Indicates that no error occurred.
+ * UnknownError indicates that an unspecified error occurred.
+ */
+ enum InstListError
+ {
+ NoError = 0,
+ UnknownError
+ };
+
+ QString instDir() const
+ {
+ return m_instDir;
+ }
+
+ /*!
+ * \brief Get the instance at index
+ */
+ InstancePtr at(int i) const
+ {
+ return m_instances.at(i);
+ }
+ ;
+
+ /*!
+ * \brief Get the count of loaded instances
+ */
+ int count() const
+ {
+ return m_instances.count();
+ }
+ ;
+
+ /// Clear all instances. Triggers notifications.
+ void clear();
+
+ /// Add an instance. Triggers notifications, returns the new index
+ int add(InstancePtr t);
+
+ /// Get an instance by ID
+ InstancePtr getInstanceById(QString id) const;
+
+ QModelIndex getInstanceIndexById(const QString &id) const;
+
+ // FIXME: instead of iterating through all instances and forming a set, keep the set around
+ QStringList getGroups();
+signals:
+ void dataIsInvalid();
+
+public
+slots:
+ void on_InstFolderChanged(const Setting &setting, QVariant value);
+
+ /*!
+ * \brief Loads the instance list. Triggers notifications.
+ */
+ InstListError loadList();
+ void loadForgeInstances(QMap<QString, QString> groupMap);
+
+private
+slots:
+ void propertiesChanged(BaseInstance *inst);
+ void instanceNuked(BaseInstance *inst);
+ void groupChanged();
+
+private:
+ int getInstIndex(BaseInstance *inst) const;
+
+ void continueProcessInstance(BaseInstance *instPtr, const int error, const QDir &dir,
+ QMap<QString, QString> &groupMap);
+
+protected:
+ QString m_instDir;
+ QList<InstancePtr> m_instances;
+ QSet<QString> m_groups;
+};
+
+class InstanceProxyModel : public KCategorizedSortFilterProxyModel
+{
+public:
+ explicit InstanceProxyModel(QObject *parent = 0);
+
+protected:
+ virtual bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const;
+};
diff --git a/logic/lists/JavaVersionList.cpp b/logic/lists/JavaVersionList.cpp
new file mode 100644
index 00000000..eb1c5650
--- /dev/null
+++ b/logic/lists/JavaVersionList.cpp
@@ -0,0 +1,242 @@
+/* Copyright 2013 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 "JavaVersionList.h"
+#include "MultiMC.h"
+
+#include <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include "logger/QsLog.h"
+#include "logic/JavaCheckerJob.h"
+#include "logic/JavaUtils.h"
+
+JavaVersionList::JavaVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *JavaVersionList::getLoadTask()
+{
+ return new JavaListLoadTask(this);
+}
+
+const BaseVersionPtr JavaVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+bool JavaVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+int JavaVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+int JavaVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 3;
+}
+
+QVariant JavaVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<JavaVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case 0:
+ return version->id;
+
+ case 1:
+ return version->arch;
+
+ case 2:
+ return version->path;
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return version->descriptor();
+
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case 0:
+ return "Version";
+
+ case 1:
+ return "Arch";
+
+ case 2:
+ return "Path";
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case 0:
+ return "The name of the version.";
+
+ case 1:
+ return "The architecture this version is for.";
+
+ case 2:
+ return "Path to this Java version.";
+
+ default:
+ return QVariant();
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionPtr JavaVersionList::getTopRecommended() const
+{
+ auto first = m_vlist.first();
+ if(first != nullptr)
+ {
+ return first;
+ }
+ else
+ {
+ return BaseVersionPtr();
+ }
+}
+
+void JavaVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ endResetModel();
+ // NOW SORT!!
+ // sort();
+}
+
+void JavaVersionList::sort()
+{
+ // NO-OP for now
+}
+
+JavaListLoadTask::JavaListLoadTask(JavaVersionList *vlist)
+{
+ m_list = vlist;
+ m_currentRecommended = NULL;
+}
+
+JavaListLoadTask::~JavaListLoadTask()
+{
+}
+
+void JavaListLoadTask::executeTask()
+{
+ setStatus(tr("Detecting Java installations..."));
+
+ JavaUtils ju;
+ QList<QString> candidate_paths = ju.FindJavaPaths();
+
+ m_job = std::shared_ptr<JavaCheckerJob>(new JavaCheckerJob("Java detection"));
+ connect(m_job.get(), SIGNAL(finished(QList<JavaCheckResult>)), this, SLOT(javaCheckerFinished(QList<JavaCheckResult>)));
+ connect(m_job.get(), SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int)));
+
+ QLOG_DEBUG() << "Probing the following Java paths: ";
+ int id = 0;
+ for(QString candidate : candidate_paths)
+ {
+ QLOG_DEBUG() << " " << candidate;
+
+ auto candidate_checker = new JavaChecker();
+ candidate_checker->path = candidate;
+ candidate_checker->id = id;
+ m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker));
+
+ id++;
+ }
+
+ m_job->start();
+}
+
+void JavaListLoadTask::checkerProgress(int current, int total)
+{
+ float progress = (current * 100.0) / total;
+ this->setProgress((int) progress);
+}
+
+void JavaListLoadTask::javaCheckerFinished(QList<JavaCheckResult> results)
+{
+ QList<JavaVersionPtr> candidates;
+ m_job.reset();
+
+ QLOG_DEBUG() << "Found the following valid Java installations:";
+ for(JavaCheckResult result : results)
+ {
+ if(result.valid)
+ {
+ JavaVersionPtr javaVersion(new JavaVersion());
+
+ javaVersion->id = result.javaVersion;
+ javaVersion->arch = result.mojangPlatform;
+ javaVersion->path = result.path;
+ candidates.append(javaVersion);
+
+ QLOG_DEBUG() << " " << javaVersion->id << javaVersion->arch << javaVersion->path;
+ }
+ }
+
+ QList<BaseVersionPtr> javas_bvp;
+ for (auto &java : candidates)
+ {
+ //QLOG_INFO() << java->id << java->arch << " at " << java->path;
+ BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
+
+ if (bp_java)
+ {
+ javas_bvp.append(bp_java);
+ }
+ }
+
+ m_list->updateListData(javas_bvp);
+ emitSucceeded();
+}
diff --git a/logic/lists/JavaVersionList.h b/logic/lists/JavaVersionList.h
new file mode 100644
index 00000000..e6cc8e5f
--- /dev/null
+++ b/logic/lists/JavaVersionList.h
@@ -0,0 +1,96 @@
+/* Copyright 2013 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 <QObject>
+#include <QAbstractListModel>
+
+#include "BaseVersionList.h"
+#include "logic/tasks/Task.h"
+#include "logic/JavaCheckerJob.h"
+
+class JavaListLoadTask;
+
+struct JavaVersion : public BaseVersion
+{
+ virtual QString descriptor()
+ {
+ return id;
+ }
+
+ virtual QString name()
+ {
+ return id;
+ }
+
+ virtual QString typeString() const
+ {
+ return arch;
+ }
+
+ QString id;
+ QString arch;
+ QString path;
+};
+
+typedef std::shared_ptr<JavaVersion> JavaVersionPtr;
+
+class JavaVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ explicit JavaVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask();
+ virtual bool isLoaded();
+ virtual const BaseVersionPtr at(int i) const;
+ virtual int count() const;
+ virtual void sort();
+
+ virtual BaseVersionPtr getTopRecommended() const;
+
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+
+public
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions);
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+};
+
+class JavaListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit JavaListLoadTask(JavaVersionList *vlist);
+ ~JavaListLoadTask();
+
+ virtual void executeTask();
+public slots:
+ void javaCheckerFinished(QList<JavaCheckResult> results);
+ void checkerProgress(int current, int total);
+
+protected:
+ std::shared_ptr<JavaCheckerJob> m_job;
+ JavaVersionList *m_list;
+ JavaVersion *m_currentRecommended;
+};
diff --git a/logic/lists/LwjglVersionList.cpp b/logic/lists/LwjglVersionList.cpp
new file mode 100644
index 00000000..df46d7be
--- /dev/null
+++ b/logic/lists/LwjglVersionList.cpp
@@ -0,0 +1,199 @@
+/* Copyright 2013 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 "LwjglVersionList.h"
+#include "MultiMC.h"
+
+#include <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include "logger/QsLog.h"
+
+#define RSS_URL "http://sourceforge.net/api/file/index/project-id/58488/mtime/desc/rss"
+
+LWJGLVersionList::LWJGLVersionList(QObject *parent) : QAbstractListModel(parent)
+{
+ setLoading(false);
+}
+
+QVariant LWJGLVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ const PtrLWJGLVersion version = at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return version->name();
+
+ case Qt::ToolTipRole:
+ return version->url();
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant LWJGLVersionList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return "Version";
+
+ case Qt::ToolTipRole:
+ return "LWJGL version name.";
+
+ default:
+ return QVariant();
+ }
+}
+
+int LWJGLVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+bool LWJGLVersionList::isLoading() const
+{
+ return m_loading;
+}
+
+void LWJGLVersionList::loadList()
+{
+ Q_ASSERT_X(!m_loading, "loadList", "list is already loading (m_loading is true)");
+
+ setLoading(true);
+ auto worker = MMC->qnam();
+ QNetworkRequest req(QUrl(RSS_URL));
+ req.setRawHeader("Accept", "text/xml");
+ req.setRawHeader("User-Agent", "MultiMC/5.0 (Uncached)");
+ reply = worker->get(req);
+ connect(reply, SIGNAL(finished()), SLOT(netRequestComplete()));
+}
+
+inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname)
+{
+ QDomNodeList elementList = parent.elementsByTagName(tagname);
+ if (elementList.count())
+ return elementList.at(0).toElement();
+ else
+ return QDomElement();
+}
+
+void LWJGLVersionList::netRequestComplete()
+{
+ if (reply->error() == QNetworkReply::NoError)
+ {
+ QRegExp lwjglRegex("lwjgl-(([0-9]\\.?)+)\\.zip");
+ Q_ASSERT_X(lwjglRegex.isValid(), "load LWJGL list", "LWJGL regex is invalid");
+
+ QDomDocument doc;
+
+ QString xmlErrorMsg;
+ int errorLine;
+ if (!doc.setContent(reply->readAll(), false, &xmlErrorMsg, &errorLine))
+ {
+ failed("Failed to load LWJGL list. XML error: " + xmlErrorMsg + " at line " +
+ QString::number(errorLine));
+ setLoading(false);
+ return;
+ }
+
+ QDomNodeList items = doc.elementsByTagName("item");
+
+ QList<PtrLWJGLVersion> tempList;
+
+ for (int i = 0; i < items.length(); i++)
+ {
+ Q_ASSERT_X(items.at(i).isElement(), "load LWJGL list",
+ "XML element isn't an element... wat?");
+
+ QDomElement linkElement = getDomElementByTagName(items.at(i).toElement(), "link");
+ if (linkElement.isNull())
+ {
+ QLOG_INFO() << "Link element" << i << "in RSS feed doesn't exist! Skipping.";
+ continue;
+ }
+
+ QString link = linkElement.text();
+
+ // Make sure it's a download link.
+ if (link.endsWith("/download") && link.contains(lwjglRegex))
+ {
+ QString name = link.mid(lwjglRegex.indexIn(link) + 6);
+ // Subtract 4 here to remove the .zip file extension.
+ name = name.left(lwjglRegex.matchedLength() - 10);
+
+ QUrl url(link);
+ if (!url.isValid())
+ {
+ QLOG_INFO() << "LWJGL version URL isn't valid:" << link << "Skipping.";
+ continue;
+ }
+
+ tempList.append(LWJGLVersion::Create(name, link));
+ }
+ }
+
+ beginResetModel();
+ m_vlist.swap(tempList);
+ endResetModel();
+
+ QLOG_INFO() << "Loaded LWJGL list.";
+ finished();
+ }
+ else
+ {
+ failed("Failed to load LWJGL list. Network error: " + reply->errorString());
+ }
+
+ setLoading(false);
+ reply->deleteLater();
+}
+
+const PtrLWJGLVersion LWJGLVersionList::getVersion(const QString &versionName)
+{
+ for (int i = 0; i < count(); i++)
+ {
+ QString name = at(i)->name();
+ if (name == versionName)
+ return at(i);
+ }
+ return PtrLWJGLVersion();
+}
+
+void LWJGLVersionList::failed(QString msg)
+{
+ QLOG_INFO() << msg;
+ emit loadListFailed(msg);
+}
+
+void LWJGLVersionList::finished()
+{
+ emit loadListFinished();
+}
+
+void LWJGLVersionList::setLoading(bool loading)
+{
+ m_loading = loading;
+ emit loadingStateUpdated(m_loading);
+}
diff --git a/logic/lists/LwjglVersionList.h b/logic/lists/LwjglVersionList.h
new file mode 100644
index 00000000..fa57e8eb
--- /dev/null
+++ b/logic/lists/LwjglVersionList.h
@@ -0,0 +1,148 @@
+/* Copyright 2013 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 <QObject>
+#include <QAbstractListModel>
+#include <QUrl>
+#include <QNetworkReply>
+
+#include <memory>
+
+class LWJGLVersion;
+typedef std::shared_ptr<LWJGLVersion> PtrLWJGLVersion;
+
+class LWJGLVersion : public QObject
+{
+ Q_OBJECT
+
+ LWJGLVersion(const QString &name, const QString &url, QObject *parent = 0)
+ : QObject(parent), m_name(name), m_url(url)
+ {
+ }
+
+public:
+
+ static PtrLWJGLVersion Create(const QString &name, const QString &url, QObject *parent = 0)
+ {
+ return PtrLWJGLVersion(new LWJGLVersion(name, url, parent));
+ }
+ ;
+
+ QString name() const
+ {
+ return m_name;
+ }
+
+ QString url() const
+ {
+ return m_url;
+ }
+
+protected:
+ QString m_name;
+ QString m_url;
+};
+
+class LWJGLVersionList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit LWJGLVersionList(QObject *parent = 0);
+
+ bool isLoaded()
+ {
+ return m_vlist.length() > 0;
+ }
+
+ const PtrLWJGLVersion getVersion(const QString &versionName);
+ PtrLWJGLVersion at(int index)
+ {
+ return m_vlist[index];
+ }
+ const PtrLWJGLVersion at(int index) const
+ {
+ return m_vlist[index];
+ }
+
+ int count() const
+ {
+ return m_vlist.length();
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ virtual int rowCount(const QModelIndex &parent) const
+ {
+ return count();
+ }
+ virtual int columnCount(const QModelIndex &parent) const;
+
+ virtual bool isLoading() const;
+ virtual bool errored() const
+ {
+ return m_errored;
+ }
+
+ virtual QString lastErrorMsg() const
+ {
+ return m_lastErrorMsg;
+ }
+
+public
+slots:
+ /*!
+ * Loads the version list.
+ * This is done asynchronously. On success, the loadListFinished() signal will
+ * be emitted. The list model will be reset as well, resulting in the modelReset()
+ * signal being emitted. Note that the model will be reset before loadListFinished() is
+ * emitted.
+ * If loading the list failed, the loadListFailed(QString msg),
+ * signal will be emitted.
+ */
+ virtual void loadList();
+
+signals:
+ /*!
+ * Emitted when the list either starts or finishes loading.
+ * \param loading Whether or not the list is loading.
+ */
+ void loadingStateUpdated(bool loading);
+
+ void loadListFinished();
+
+ void loadListFailed(QString msg);
+
+private:
+ QList<PtrLWJGLVersion> m_vlist;
+
+ QNetworkReply *m_netReply;
+ QNetworkReply *reply;
+
+ bool m_loading;
+ bool m_errored;
+ QString m_lastErrorMsg;
+
+ void failed(QString msg);
+
+ void finished();
+
+ void setLoading(bool loading);
+
+private
+slots:
+ virtual void netRequestComplete();
+};
diff --git a/logic/lists/MinecraftVersionList.cpp b/logic/lists/MinecraftVersionList.cpp
new file mode 100644
index 00000000..91f86df0
--- /dev/null
+++ b/logic/lists/MinecraftVersionList.cpp
@@ -0,0 +1,286 @@
+/* Copyright 2013 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 "MinecraftVersionList.h"
+#include "MultiMC.h"
+#include "logic/net/URLConstants.h"
+
+#include <QtXml>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QJsonParseError>
+
+#include <QtAlgorithms>
+
+#include <QtNetwork>
+
+MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *MinecraftVersionList::getLoadTask()
+{
+ return new MCVListLoadTask(this);
+}
+
+bool MinecraftVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr MinecraftVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int MinecraftVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second)
+{
+ auto left = std::dynamic_pointer_cast<MinecraftVersion>(first);
+ auto right = std::dynamic_pointer_cast<MinecraftVersion>(second);
+ return left->timestamp > right->timestamp;
+}
+
+void MinecraftVersionList::sort()
+{
+ beginResetModel();
+ qSort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+ endResetModel();
+}
+
+BaseVersionPtr MinecraftVersionList::getLatestStable() const
+{
+ for (int i = 0; i < m_vlist.length(); i++)
+ {
+ auto ver = std::dynamic_pointer_cast<MinecraftVersion>(m_vlist.at(i));
+ if (ver->is_latest && !ver->is_snapshot)
+ {
+ return m_vlist.at(i);
+ }
+ }
+ return BaseVersionPtr();
+}
+
+void MinecraftVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ endResetModel();
+ // NOW SORT!!
+ sort();
+}
+
+inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname)
+{
+ QDomNodeList elementList = parent.elementsByTagName(tagname);
+ if (elementList.count())
+ return elementList.at(0).toElement();
+ else
+ return QDomElement();
+}
+
+inline QDateTime timeFromS3Time(QString str)
+{
+ return QDateTime::fromString(str, Qt::ISODate);
+}
+
+MCVListLoadTask::MCVListLoadTask(MinecraftVersionList *vlist)
+{
+ m_list = vlist;
+ m_currentStable = NULL;
+ vlistReply = nullptr;
+ legacyWhitelist.insert("1.5.2");
+ legacyWhitelist.insert("1.5.1");
+ legacyWhitelist.insert("1.5");
+ legacyWhitelist.insert("1.4.7");
+ legacyWhitelist.insert("1.4.6");
+ legacyWhitelist.insert("1.4.5");
+ legacyWhitelist.insert("1.4.4");
+ legacyWhitelist.insert("1.4.3");
+ legacyWhitelist.insert("1.4.2");
+ legacyWhitelist.insert("1.4.1");
+ legacyWhitelist.insert("1.4");
+ legacyWhitelist.insert("1.3.2");
+ legacyWhitelist.insert("1.3.1");
+ legacyWhitelist.insert("1.3");
+ legacyWhitelist.insert("1.2.5");
+ legacyWhitelist.insert("1.2.4");
+ legacyWhitelist.insert("1.2.3");
+ legacyWhitelist.insert("1.2.2");
+ legacyWhitelist.insert("1.2.1");
+ legacyWhitelist.insert("1.1");
+ legacyWhitelist.insert("1.0.1");
+ legacyWhitelist.insert("1.0");
+}
+
+MCVListLoadTask::~MCVListLoadTask()
+{
+}
+
+void MCVListLoadTask::executeTask()
+{
+ setStatus(tr("Loading instance version list..."));
+ auto worker = MMC->qnam();
+ vlistReply = worker->get(QNetworkRequest(QUrl("http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + "versions.json")));
+ connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded()));
+}
+
+void MCVListLoadTask::list_downloaded()
+{
+ if (vlistReply->error() != QNetworkReply::NoError)
+ {
+ vlistReply->deleteLater();
+ emitFailed("Failed to load Minecraft main version list" + vlistReply->errorString());
+ return;
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(vlistReply->readAll(), &jsonError);
+ vlistReply->deleteLater();
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing version list JSON:" + jsonError.errorString());
+ return;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing version list JSON: jsonDoc is not an object");
+ return;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // Get the ID of the latest release and the latest snapshot.
+ if (!root.value("latest").isObject())
+ {
+ emitFailed("Error parsing version list JSON: version list is missing 'latest' object");
+ return;
+ }
+
+ QJsonObject latest = root.value("latest").toObject();
+
+ QString latestReleaseID = latest.value("release").toString("");
+ QString latestSnapshotID = latest.value("snapshot").toString("");
+ if (latestReleaseID.isEmpty())
+ {
+ emitFailed("Error parsing version list JSON: latest release field is missing");
+ return;
+ }
+ if (latestSnapshotID.isEmpty())
+ {
+ emitFailed("Error parsing version list JSON: latest snapshot field is missing");
+ return;
+ }
+
+ // Now, get the array of versions.
+ if (!root.value("versions").isArray())
+ {
+ emitFailed(
+ "Error parsing version list JSON: version list object is missing 'versions' array");
+ return;
+ }
+ QJsonArray versions = root.value("versions").toArray();
+
+ QList<BaseVersionPtr> tempList;
+ for (int i = 0; i < versions.count(); i++)
+ {
+ bool is_snapshot = false;
+ bool is_latest = false;
+
+ // Load the version info.
+ if (!versions[i].isObject())
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+ QJsonObject version = versions[i].toObject();
+ QString versionID = version.value("id").toString("");
+ QString versionTimeStr = version.value("releaseTime").toString("");
+ QString versionTypeStr = version.value("type").toString("");
+ if (versionID.isEmpty() || versionTimeStr.isEmpty() || versionTypeStr.isEmpty())
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+
+ // Parse the timestamp.
+ QDateTime versionTime = timeFromS3Time(versionTimeStr);
+ if (!versionTime.isValid())
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+ // Parse the type.
+ MinecraftVersion::VersionType versionType;
+ // OneSix or Legacy. use filter to determine type
+ if (versionTypeStr == "release")
+ {
+ versionType = legacyWhitelist.contains(versionID) ? MinecraftVersion::Legacy
+ : MinecraftVersion::OneSix;
+ is_latest = (versionID == latestReleaseID);
+ is_snapshot = false;
+ }
+ else if (versionTypeStr == "snapshot") // It's a snapshot... yay
+ {
+ versionType = legacyWhitelist.contains(versionID) ? MinecraftVersion::Legacy
+ : MinecraftVersion::OneSix;
+ is_latest = (versionID == latestSnapshotID);
+ is_snapshot = true;
+ }
+ else if (versionTypeStr == "old_alpha")
+ {
+ versionType = MinecraftVersion::Nostalgia;
+ is_latest = false;
+ is_snapshot = false;
+ }
+ else if (versionTypeStr == "old_beta")
+ {
+ versionType = MinecraftVersion::Legacy;
+ is_latest = false;
+ is_snapshot = false;
+ }
+ else
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+ // Get the download URL.
+ QString dlUrl = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + versionID + "/";
+
+ // Now, we construct the version object and add it to the list.
+ std::shared_ptr<MinecraftVersion> mcVersion(new MinecraftVersion());
+ mcVersion->m_name = mcVersion->m_descriptor = versionID;
+ mcVersion->timestamp = versionTime.toMSecsSinceEpoch();
+ mcVersion->download_url = dlUrl;
+ mcVersion->is_latest = is_latest;
+ mcVersion->is_snapshot = is_snapshot;
+ mcVersion->type = versionType;
+ tempList.append(mcVersion);
+ }
+ m_list->updateListData(tempList);
+
+ emitSucceeded();
+ return;
+}
diff --git a/logic/lists/MinecraftVersionList.h b/logic/lists/MinecraftVersionList.h
new file mode 100644
index 00000000..82af1009
--- /dev/null
+++ b/logic/lists/MinecraftVersionList.h
@@ -0,0 +1,74 @@
+/* Copyright 2013 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 <QObject>
+#include <QList>
+#include <QSet>
+
+#include "BaseVersionList.h"
+#include "logic/tasks/Task.h"
+#include "logic/MinecraftVersion.h"
+
+class MCVListLoadTask;
+class QNetworkReply;
+
+class MinecraftVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ friend class MCVListLoadTask;
+
+ explicit MinecraftVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask();
+ virtual bool isLoaded();
+ virtual const BaseVersionPtr at(int i) const;
+ virtual int count() const;
+ virtual void sort();
+
+ virtual BaseVersionPtr getLatestStable() const;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions);
+};
+
+class MCVListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit MCVListLoadTask(MinecraftVersionList *vlist);
+ ~MCVListLoadTask();
+
+ virtual void executeTask();
+
+protected
+slots:
+ void list_downloaded();
+
+protected:
+ QNetworkReply *vlistReply;
+ MinecraftVersionList *m_list;
+ MinecraftVersion *m_currentStable;
+ QSet<QString> legacyWhitelist;
+};