summaryrefslogtreecommitdiffstats
path: root/backend/lists
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2013-07-29 00:59:35 +0200
committerPetr Mrázek <peterix@gmail.com>2013-07-29 00:59:35 +0200
commit2e0cbf393a5320dbf5448ca44a9b5905314b0be8 (patch)
tree4baac9cf015ca7b15d83de33c705e0d8d4497d30 /backend/lists
parent8808a8b108b82916eaf30f9aca50cd3ab16af230 (diff)
downloadMultiMC-2e0cbf393a5320dbf5448ca44a9b5905314b0be8.tar
MultiMC-2e0cbf393a5320dbf5448ca44a9b5905314b0be8.tar.gz
MultiMC-2e0cbf393a5320dbf5448ca44a9b5905314b0be8.tar.lz
MultiMC-2e0cbf393a5320dbf5448ca44a9b5905314b0be8.tar.xz
MultiMC-2e0cbf393a5320dbf5448ca44a9b5905314b0be8.zip
Massive renaming in the backend folder, all around restructure in the same.
Diffstat (limited to 'backend/lists')
-rw-r--r--backend/lists/InstVersionList.cpp129
-rw-r--r--backend/lists/InstVersionList.h121
-rw-r--r--backend/lists/InstanceList.cpp232
-rw-r--r--backend/lists/InstanceList.h92
-rw-r--r--backend/lists/LwjglVersionList.cpp204
-rw-r--r--backend/lists/LwjglVersionList.h132
-rw-r--r--backend/lists/MinecraftVersionList.cpp528
-rw-r--r--backend/lists/MinecraftVersionList.h106
8 files changed, 1544 insertions, 0 deletions
diff --git a/backend/lists/InstVersionList.cpp b/backend/lists/InstVersionList.cpp
new file mode 100644
index 00000000..c65770a9
--- /dev/null
+++ b/backend/lists/InstVersionList.cpp
@@ -0,0 +1,129 @@
+/* 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 "InstVersionList.h"
+#include "InstanceVersion.h"
+
+InstVersionList::InstVersionList(QObject *parent) :
+ QAbstractListModel(parent)
+{
+}
+
+const InstVersion *InstVersionList::findVersion(const QString &descriptor)
+{
+ for (int i = 0; i < count(); i++)
+ {
+ if (at(i)->descriptor() == descriptor)
+ return at(i);
+ }
+ return NULL;
+}
+
+const InstVersion *InstVersionList::getLatestStable() const
+{
+ if (count() <= 0)
+ return NULL;
+ else
+ return at(0);
+}
+
+QVariant InstVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+
+ const InstVersion *version = at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case NameColumn:
+ return version->name();
+
+ case TypeColumn:
+ return version->typeName();
+
+ case TimeColumn:
+ return version->timestamp();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return version->descriptor();
+
+ case VersionPointerRole:
+ return qVariantFromValue((void *) version);
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant InstVersionList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case NameColumn:
+ return "Name";
+
+ case TypeColumn:
+ return "Type";
+
+ case TimeColumn:
+ return "Time";
+
+ 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 InstVersionList::rowCount(const QModelIndex &parent) const
+{
+ // Return count
+ return count();
+}
+
+int InstVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 2;
+}
diff --git a/backend/lists/InstVersionList.h b/backend/lists/InstVersionList.h
new file mode 100644
index 00000000..97e00383
--- /dev/null
+++ b/backend/lists/InstVersionList.h
@@ -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.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+
+#include "libmmc_config.h"
+
+class InstVersion;
+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 LIBMULTIMC_EXPORT InstVersionList : 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 InstVersionList(QObject *parent = 0);
+
+ /*!
+ * \brief Gets a task that will reload the version islt.
+ * 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 InstVersion *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 const InstVersion *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 const InstVersion *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<InstVersion *> versions) = 0;
+};
diff --git a/backend/lists/InstanceList.cpp b/backend/lists/InstanceList.cpp
new file mode 100644
index 00000000..101d52c5
--- /dev/null
+++ b/backend/lists/InstanceList.cpp
@@ -0,0 +1,232 @@
+/* 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 <QFile>
+#include <QDirIterator>
+#include <QThread>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "lists/InstanceList.h"
+#include "BaseInstance.h"
+#include "InstanceFactory.h"
+
+#include "pathutils.h"
+
+const static int GROUP_FILE_FORMAT_VERSION = 1;
+
+InstanceList::InstanceList(const QString &instDir, QObject *parent) :
+ QObject(parent), m_instDir("instances")
+{
+
+}
+
+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.
+ qDebug("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)
+ {
+ qWarning(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())
+ {
+ qWarning("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())
+ {
+ qWarning("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())
+ {
+ qWarning(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())
+ {
+ qWarning(QString("Group '%1' in the group list is invalid. "
+ "It should contain an array "
+ "called 'instances'.").arg(groupName).toUtf8());
+ continue;
+ }
+
+ // 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;
+ }
+ }
+}
+
+InstanceList::InstListError InstanceList::loadList()
+{
+ // load the instance groups
+ QMap<QString, QString> groupMap;
+ loadGroupList(groupMap);
+
+ m_instances.clear();
+ QDir dir(m_instDir);
+ QDirIterator iter(dir);
+ while (iter.hasNext())
+ {
+ QString subDir = iter.next();
+ if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists())
+ continue;
+
+ BaseInstance *instPtr = NULL;
+ auto &loader = InstanceFactory::get();
+ auto error = loader.loadInstance(instPtr, subDir);
+
+ switch(error)
+ {
+ case InstanceFactory::NoLoadError:
+ break;
+ case InstanceFactory::NotAnInstance:
+ break;
+ }
+
+ if (error != InstanceFactory::NoLoadError &&
+ error != InstanceFactory::NotAnInstance)
+ {
+ QString errorMsg = QString("Failed to load instance %1: ").
+ arg(QFileInfo(subDir).baseName()).toUtf8();
+
+ switch (error)
+ {
+ default:
+ errorMsg += QString("Unknown instance loader error %1").
+ arg(error);
+ break;
+ }
+ qDebug(errorMsg.toUtf8());
+ }
+ else if (!instPtr)
+ {
+ qDebug(QString("Error loading instance %1. Instance loader returned null.").
+ arg(QFileInfo(subDir).baseName()).toUtf8());
+ }
+ else
+ {
+ QSharedPointer<BaseInstance> inst(instPtr);
+ auto iter = groupMap.find(inst->id());
+ if(iter != groupMap.end())
+ {
+ inst->setGroup((*iter));
+ }
+ qDebug(QString("Loaded instance %1").arg(inst->name()).toUtf8());
+ inst->setParent(this);
+ m_instances.append(inst);
+ connect(instPtr, SIGNAL(propertiesChanged(BaseInstance*)),this, SLOT(propertiesChanged(BaseInstance*)));
+ }
+ }
+ emit invalidated();
+ return NoError;
+}
+
+/// Clear all instances. Triggers notifications.
+void InstanceList::clear()
+{
+ m_instances.clear();
+ emit invalidated();
+};
+
+/// Add an instance. Triggers notifications, returns the new index
+int InstanceList::add(InstancePtr t)
+{
+ m_instances.append(t);
+ emit instanceAdded(count() - 1);
+ return count() - 1;
+}
+
+InstancePtr InstanceList::getInstanceById(QString instId)
+{
+ 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();
+}
+
+void InstanceList::propertiesChanged(BaseInstance * inst)
+{
+ for(int i = 0; i < m_instances.count(); i++)
+ {
+ if(inst == m_instances[i].data())
+ {
+ emit instanceChanged(i);
+ break;
+ }
+ }
+}
diff --git a/backend/lists/InstanceList.h b/backend/lists/InstanceList.h
new file mode 100644
index 00000000..8c9965e5
--- /dev/null
+++ b/backend/lists/InstanceList.h
@@ -0,0 +1,92 @@
+/* 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 <QSharedPointer>
+
+#include "BaseInstance.h"
+#include "libmmc_config.h"
+
+class BaseInstance;
+
+class LIBMULTIMC_EXPORT InstanceList : public QObject
+{
+ Q_OBJECT
+private:
+ /*!
+ * \brief Get the instance groups
+ */
+ void loadGroupList(QMap<QString, QString> & groupList);
+
+public:
+ explicit InstanceList(const QString &instDir, QObject *parent = 0);
+
+ /*!
+ * \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 Loads the instance list. Triggers notifications.
+ */
+ InstListError loadList();
+
+ /*!
+ * \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);
+
+signals:
+ void instanceAdded(int index);
+ void instanceChanged(int index);
+ void invalidated();
+
+private slots:
+ void propertiesChanged(BaseInstance * inst);
+
+protected:
+ QString m_instDir;
+ QList< InstancePtr > m_instances;
+};
diff --git a/backend/lists/LwjglVersionList.cpp b/backend/lists/LwjglVersionList.cpp
new file mode 100644
index 00000000..824d0906
--- /dev/null
+++ b/backend/lists/LwjglVersionList.cpp
@@ -0,0 +1,204 @@
+/* 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 <QtNetwork>
+
+#include <QtXml>
+
+#include <QRegExp>
+
+#define RSS_URL "http://sourceforge.net/api/file/index/project-id/58488/mtime/desc/rss"
+
+LWJGLVersionList mainVersionList;
+
+LWJGLVersionList &LWJGLVersionList::get()
+{
+ return mainVersionList;
+}
+
+
+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().toString();
+
+ 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);
+ reply = netMgr.get(QNetworkRequest(QUrl(RSS_URL)));
+ 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())
+ {
+ qWarning() << "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));
+ // Subtract 4 here to remove the .zip file extension.
+ name = name.left(lwjglRegex.matchedLength() - 4);
+
+ QUrl url(link);
+ if (!url.isValid())
+ {
+ qWarning() << "LWJGL version URL isn't valid:" << link << "Skipping.";
+ continue;
+ }
+
+ tempList.append(LWJGLVersion::Create(name, link));
+ }
+ }
+
+ beginResetModel();
+ m_vlist.swap(tempList);
+ endResetModel();
+
+ qDebug("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++)
+ {
+ if (at(i)->name() == versionName)
+ return at(i);
+ }
+ return PtrLWJGLVersion();
+}
+
+
+void LWJGLVersionList::failed(QString msg)
+{
+ qWarning() << msg;
+ emit loadListFailed(msg);
+}
+
+void LWJGLVersionList::finished()
+{
+ emit loadListFinished();
+}
+
+void LWJGLVersionList::setLoading(bool loading)
+{
+ m_loading = loading;
+ emit loadingStateUpdated(m_loading);
+}
diff --git a/backend/lists/LwjglVersionList.h b/backend/lists/LwjglVersionList.h
new file mode 100644
index 00000000..f3e7799a
--- /dev/null
+++ b/backend/lists/LwjglVersionList.h
@@ -0,0 +1,132 @@
+/* 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 <QSharedPointer>
+#include <QUrl>
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include "libmmc_config.h"
+
+class LWJGLVersion;
+typedef QSharedPointer<LWJGLVersion> PtrLWJGLVersion;
+
+class LIBMULTIMC_EXPORT LWJGLVersion : public QObject
+{
+ Q_OBJECT
+
+ /*!
+ * The name of the LWJGL version.
+ */
+ Q_PROPERTY(QString name READ name)
+
+ /*!
+ * The URL for this version of LWJGL.
+ */
+ Q_PROPERTY(QUrl url READ url)
+
+ LWJGLVersion(const QString &name, const QUrl &url, QObject *parent = 0) :
+ QObject(parent), m_name(name), m_url(url) { }
+public:
+
+ static PtrLWJGLVersion Create(const QString &name, const QUrl &url, QObject *parent = 0)
+ {
+ return PtrLWJGLVersion(new LWJGLVersion(name, url, parent));
+ };
+
+ QString name() const { return m_name; }
+
+ QUrl url() const { return m_url; }
+
+protected:
+ QString m_name;
+ QUrl m_url;
+};
+
+class LIBMULTIMC_EXPORT LWJGLVersionList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit LWJGLVersionList(QObject *parent = 0);
+
+ static LWJGLVersionList &get();
+
+ 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;
+
+ QNetworkAccessManager netMgr;
+ 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/backend/lists/MinecraftVersionList.cpp b/backend/lists/MinecraftVersionList.cpp
new file mode 100644
index 00000000..d576397f
--- /dev/null
+++ b/backend/lists/MinecraftVersionList.cpp
@@ -0,0 +1,528 @@
+/* Copyright 2013 Andrew Okin
+ *
+ * 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 <QDebug>
+
+#include <QtXml>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QJsonParseError>
+
+#include <QtAlgorithms>
+
+#include <QtNetwork>
+
+#include "netutils.h"
+
+#define MCVLIST_URLBASE "http://s3.amazonaws.com/Minecraft.Download/versions/"
+#define ASSETS_URLBASE "http://assets.minecraft.net/"
+#define MCN_URLBASE "http://sonicrules.org/mcnweb.py"
+
+MinecraftVersionList mcVList;
+
+MinecraftVersionList::MinecraftVersionList(QObject *parent) :
+ InstVersionList(parent)
+{
+
+}
+
+Task *MinecraftVersionList::getLoadTask()
+{
+ return new MCVListLoadTask(this);
+}
+
+bool MinecraftVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const InstVersion *MinecraftVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int MinecraftVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+void MinecraftVersionList::printToStdOut() const
+{
+ qDebug() << "---------------- Version List ----------------";
+
+ for (int i = 0; i < m_vlist.count(); i++)
+ {
+ MinecraftVersion *version = qobject_cast<MinecraftVersion *>(m_vlist.at(i));
+
+ if (!version)
+ continue;
+
+ qDebug() << "Version " << version->name();
+ qDebug() << "\tDownload: " << version->downloadURL();
+ qDebug() << "\tTimestamp: " << version->timestamp();
+ qDebug() << "\tType: " << version->typeName();
+ qDebug() << "----------------------------------------------";
+ }
+}
+
+bool cmpVersions(const InstVersion *first, const InstVersion *second)
+{
+ return !first->isLessThan(*second);
+}
+
+void MinecraftVersionList::sort()
+{
+ beginResetModel();
+ qSort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+ endResetModel();
+}
+
+InstVersion *MinecraftVersionList::getLatestStable() const
+{
+ for (int i = 0; i < m_vlist.length(); i++)
+ {
+ if (((MinecraftVersion *)m_vlist.at(i))->versionType() == MinecraftVersion::CurrentStable)
+ {
+ return m_vlist.at(i);
+ }
+ }
+ return NULL;
+}
+
+MinecraftVersionList &MinecraftVersionList::getMainList()
+{
+ return mcVList;
+}
+
+void MinecraftVersionList::updateListData(QList<InstVersion *> versions)
+{
+ // First, we populate a temporary list with the copies of the versions.
+ QList<InstVersion *> tempList;
+ for (int i = 0; i < versions.length(); i++)
+ {
+ InstVersion *version = versions[i]->copyVersion(this);
+ Q_ASSERT(version != NULL);
+ tempList.append(version);
+ }
+
+ // Now we swap the temporary list into the actual version list.
+ // This applies our changes to the version list immediately and still gives us
+ // access to the old version list so that we can delete the objects in it and
+ // free their memory. By doing this, we cause the version list to update as
+ // quickly as possible.
+ beginResetModel();
+ m_vlist.swap(tempList);
+ m_loaded = true;
+ endResetModel();
+
+ // We called swap, so all the data that was in the version list previously is now in
+ // tempList (and vice-versa). Now we just free the memory.
+ while (!tempList.isEmpty())
+ delete tempList.takeFirst();
+
+ // 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;
+ processedAssetsReply = false;
+ processedMCNReply = false;
+ processedMCVListReply = false;
+}
+
+MCVListLoadTask::~MCVListLoadTask()
+{
+// delete netMgr;
+}
+
+void MCVListLoadTask::executeTask()
+{
+ setSubStatus();
+
+ QNetworkAccessManager networkMgr;
+ netMgr = &networkMgr;
+
+ if (!loadFromVList())
+ {
+ qDebug() << "Failed to load from Mojang version list.";
+ }
+ if (!loadFromAssets())
+ {
+ qDebug() << "Failed to load assets version list.";
+ }
+ if (!loadMCNostalgia())
+ {
+ qDebug() << "Failed to load MCNostalgia version list.";
+ }
+ finalize();
+}
+
+void MCVListLoadTask::setSubStatus(const QString msg)
+{
+ if (msg.isEmpty())
+ setStatus("Loading instance version list...");
+ else
+ setStatus("Loading instance version list: " + msg);
+}
+
+// FIXME: we should have a local cache of the version list and a local cache of version data
+bool MCVListLoadTask::loadFromVList()
+{
+ QNetworkReply *vlistReply = netMgr->get(QNetworkRequest(QUrl(QString(MCVLIST_URLBASE) +
+ "versions.json")));
+ NetUtils::waitForNetRequest(vlistReply);
+
+ switch (vlistReply->error())
+ {
+ case QNetworkReply::NoError:
+ {
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(vlistReply->readAll(), &jsonError);
+
+ if (jsonError.error == QJsonParseError::NoError)
+ {
+ Q_ASSERT_X(jsonDoc.isObject(), "loadFromVList", "jsonDoc is not an object");
+
+ QJsonObject root = jsonDoc.object();
+
+ // Get the ID of the latest release and the latest snapshot.
+ Q_ASSERT_X(root.value("latest").isObject(), "loadFromVList",
+ "version list is missing 'latest' object");
+ QJsonObject latest = root.value("latest").toObject();
+
+ QString latestReleaseID = latest.value("release").toString("");
+ QString latestSnapshotID = latest.value("snapshot").toString("");
+ Q_ASSERT_X(!latestReleaseID.isEmpty(), "loadFromVList", "latest release field is missing");
+ Q_ASSERT_X(!latestSnapshotID.isEmpty(), "loadFromVList", "latest snapshot field is missing");
+
+ // Now, get the array of versions.
+ Q_ASSERT_X(root.value("versions").isArray(), "loadFromVList",
+ "version list object is missing 'versions' array");
+ QJsonArray versions = root.value("versions").toArray();
+
+ for (int i = 0; i < versions.count(); i++)
+ {
+ // Load the version info.
+ Q_ASSERT_X(versions[i].isObject(), "loadFromVList",
+ QString("in versions array, index %1 is not an object").
+ arg(i).toUtf8());
+ QJsonObject version = versions[i].toObject();
+
+ QString versionID = version.value("id").toString("");
+ QString versionTimeStr = version.value("releaseTime").toString("");
+ QString versionTypeStr = version.value("type").toString("");
+
+ Q_ASSERT_X(!versionID.isEmpty(), "loadFromVList",
+ QString("in versions array, index %1's \"id\" field is not a valid string").
+ arg(i).toUtf8());
+ Q_ASSERT_X(!versionTimeStr.isEmpty(), "loadFromVList",
+ QString("in versions array, index %1's \"time\" field is not a valid string").
+ arg(i).toUtf8());
+ Q_ASSERT_X(!versionTypeStr.isEmpty(), "loadFromVList",
+ QString("in versions array, index %1's \"type\" field is not a valid string").
+ arg(i).toUtf8());
+
+
+ // Now, process that info and add the version to the list.
+
+ // Parse the timestamp.
+ QDateTime versionTime = timeFromS3Time(versionTimeStr);
+
+ Q_ASSERT_X(versionTime.isValid(), "loadFromVList",
+ QString("in versions array, index %1's timestamp failed to parse").
+ arg(i).toUtf8());
+
+ // Parse the type.
+ MinecraftVersion::VersionType versionType;
+ if (versionTypeStr == "release")
+ {
+ // Check if this version is the current stable version.
+ if (versionID == latestReleaseID)
+ versionType = MinecraftVersion::CurrentStable;
+ else
+ versionType = MinecraftVersion::Stable;
+ }
+ else if(versionTypeStr == "snapshot")
+ {
+ versionType = MinecraftVersion::Snapshot;
+ }
+ else
+ {
+ // we don't know what to do with this...
+ continue;
+ }
+
+ // Get the download URL.
+ QString dlUrl = QString(MCVLIST_URLBASE) + versionID + "/";
+
+
+ // Now, we construct the version object and add it to the list.
+ MinecraftVersion *mcVersion = new MinecraftVersion(
+ versionID, versionID, versionTime.toMSecsSinceEpoch(),
+ dlUrl, "");
+ mcVersion->setVersionSource(MinecraftVersion::Launcher16);
+ mcVersion->setVersionType(versionType);
+ tempList.append(mcVersion);
+ }
+ }
+ else
+ {
+ qDebug() << "Error parsing version list JSON:" << jsonError.errorString();
+ }
+
+ break;
+ }
+
+ default:
+ // TODO: Network error handling.
+ qDebug() << "Failed to load Minecraft main version list" << vlistReply->errorString();
+ break;
+ }
+
+ return true;
+}
+
+bool MCVListLoadTask::loadFromAssets()
+{
+ setSubStatus("Loading versions from assets.minecraft.net...");
+
+ bool succeeded = false;
+
+ QNetworkReply *assetsReply = netMgr->get(QNetworkRequest(QUrl(ASSETS_URLBASE)));
+ NetUtils::waitForNetRequest(assetsReply);
+
+ switch (assetsReply->error())
+ {
+ case QNetworkReply::NoError:
+ {
+ // Get the XML string.
+ QString xmlString = assetsReply->readAll();
+
+ QString xmlErrorMsg;
+
+ QDomDocument doc;
+ if (!doc.setContent(xmlString, false, &xmlErrorMsg))
+ {
+ // TODO: Display error message to the user.
+ qDebug() << "Failed to process assets.minecraft.net. XML error:" <<
+ xmlErrorMsg << xmlString;
+ }
+
+ QDomNodeList contents = doc.elementsByTagName("Contents");
+
+ QRegExp mcRegex("/minecraft.jar$");
+ QRegExp snapshotRegex("[0-9][0-9]w[0-9][0-9][a-z]?|pre|rc");
+
+ for (int i = 0; i < contents.length(); i++)
+ {
+ QDomElement element = contents.at(i).toElement();
+
+ if (element.isNull())
+ continue;
+
+ QDomElement keyElement = getDomElementByTagName(element, "Key");
+ QDomElement lastmodElement = getDomElementByTagName(element, "LastModified");
+ QDomElement etagElement = getDomElementByTagName(element, "ETag");
+
+ if (keyElement.isNull() || lastmodElement.isNull() || etagElement.isNull())
+ continue;
+
+ QString key = keyElement.text();
+ QString lastModStr = lastmodElement.text();
+ QString etagStr = etagElement.text();
+
+ if (!key.contains(mcRegex))
+ continue;
+
+ QString versionDirName = key.left(key.length() - 14);
+ QString dlUrl = QString("http://assets.minecraft.net/%1/").arg(versionDirName);
+
+ QString versionName = versionDirName.replace("_", ".");
+
+ QDateTime versionTimestamp = timeFromS3Time(lastModStr);
+ if (!versionTimestamp.isValid())
+ {
+ qDebug(QString("Failed to parse timestamp for version %1 %2").
+ arg(versionName, lastModStr).toUtf8());
+ versionTimestamp = QDateTime::currentDateTime();
+ }
+
+ if (m_currentStable)
+ {
+ {
+ bool older = versionTimestamp.toMSecsSinceEpoch() < m_currentStable->timestamp();
+ bool newer = versionTimestamp.toMSecsSinceEpoch() > m_currentStable->timestamp();
+ bool isSnapshot = versionName.contains(snapshotRegex);
+
+ MinecraftVersion *version = new MinecraftVersion(
+ versionName, versionName,
+ versionTimestamp.toMSecsSinceEpoch(),
+ dlUrl, etagStr);
+
+ if (newer)
+ {
+ version->setVersionType(MinecraftVersion::Snapshot);
+ }
+ else if (older && isSnapshot)
+ {
+ version->setVersionType(MinecraftVersion::OldSnapshot);
+ }
+ else if (older)
+ {
+ version->setVersionType(MinecraftVersion::Stable);
+ }
+ else
+ {
+ // Shouldn't happen, but just in case...
+ version->setVersionType(MinecraftVersion::CurrentStable);
+ }
+
+ assetsList.push_back(version);
+ }
+ }
+ else // If there isn't a current stable version.
+ {
+ bool isSnapshot = versionName.contains(snapshotRegex);
+
+ MinecraftVersion *version = new MinecraftVersion(
+ versionName, versionName,
+ versionTimestamp.toMSecsSinceEpoch(),
+ dlUrl, etagStr);
+ version->setVersionType(isSnapshot? MinecraftVersion::Snapshot :
+ MinecraftVersion::Stable);
+ assetsList.push_back(version);
+ }
+ }
+
+ setSubStatus("Loaded assets.minecraft.net");
+ succeeded = true;
+ break;
+ }
+
+ default:
+ // TODO: Network error handling.
+ qDebug() << "Failed to load assets.minecraft.net" << assetsReply->errorString();
+ break;
+ }
+
+ processedAssetsReply = true;
+ updateStuff();
+ return succeeded;
+}
+
+bool MCVListLoadTask::loadMCNostalgia()
+{
+ QNetworkReply *mcnReply = netMgr->get(QNetworkRequest(QUrl(QString(MCN_URLBASE) + "?pversion=1&list=True")));
+ NetUtils::waitForNetRequest(mcnReply);
+ processedMCNReply = true;
+ updateStuff();
+ return true;
+}
+
+bool MCVListLoadTask::finalize()
+{
+ // First, we need to do some cleanup. We loaded assets versions into assetsList,
+ // MCNostalgia versions into mcnList and all the others into tempList. MCNostalgia
+ // provides some versions that are on assets.minecraft.net and we want to ignore
+ // those, so we remove and delete them from mcnList. assets.minecraft.net also provides
+ // versions that are on Mojang's version list and we want to ignore those as well.
+
+ // To start, we get a list of the descriptors in tmpList.
+ QStringList tlistDescriptors;
+ for (int i = 0; i < tempList.count(); i++)
+ tlistDescriptors.append(tempList.at(i)->descriptor());
+
+ // Now, we go through our assets version list and remove anything with
+ // a descriptor that matches one we already have in tempList.
+ for (int i = 0; i < assetsList.count(); i++)
+ if (tlistDescriptors.contains(assetsList.at(i)->descriptor()))
+ delete assetsList.takeAt(i--); // We need to decrement here because we're removing an item.
+
+ // We also need to rebuild the list of descriptors.
+ tlistDescriptors.clear();
+ for (int i = 0; i < tempList.count(); i++)
+ tlistDescriptors.append(tempList.at(i)->descriptor());
+
+ // Next, we go through our MCNostalgia version list and do the same thing.
+ for (int i = 0; i < mcnList.count(); i++)
+ if (tlistDescriptors.contains(mcnList.at(i)->descriptor()))
+ delete mcnList.takeAt(i--); // We need to decrement here because we're removing an item.
+
+ // Now that the duplicates are gone, we need to merge the lists. This is
+ // simple enough.
+ tempList.append(assetsList);
+ tempList.append(mcnList);
+
+ // We're done with these lists now, but the items have been moved over to
+ // tempList, so we don't need to delete them yet.
+
+ // Now, we invoke the updateListData slot on the GUI thread. This will copy all
+ // the versions we loaded and set their parents to the version list.
+ // Then, it will swap the new list with the old one and free the old list's memory.
+ QMetaObject::invokeMethod(m_list, "updateListData", Qt::BlockingQueuedConnection,
+ Q_ARG(QList<InstVersion*>, tempList));
+
+ // Once that's finished, we can delete the versions in our temp list.
+ while (!tempList.isEmpty())
+ delete tempList.takeFirst();
+
+#ifdef PRINT_VERSIONS
+ m_list->printToStdOut();
+#endif
+ return true;
+}
+
+void MCVListLoadTask::updateStuff()
+{
+ const int totalReqs = 3;
+ int reqsComplete = 0;
+
+ if (processedMCVListReply)
+ reqsComplete++;
+ if (processedAssetsReply)
+ reqsComplete++;
+ if (processedMCNReply)
+ reqsComplete++;
+
+ calcProgress(reqsComplete, totalReqs);
+
+ if (reqsComplete >= totalReqs)
+ {
+ quit();
+ }
+}
diff --git a/backend/lists/MinecraftVersionList.h b/backend/lists/MinecraftVersionList.h
new file mode 100644
index 00000000..10570b01
--- /dev/null
+++ b/backend/lists/MinecraftVersionList.h
@@ -0,0 +1,106 @@
+/* Copyright 2013 Andrew Okin
+ *
+ * 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 <QNetworkAccessManager>
+
+#include <QList>
+
+#include "InstVersionList.h"
+
+#include "tasks/Task.h"
+
+#include "MinecraftVersion.h"
+
+#include "libmmc_config.h"
+
+class MCVListLoadTask;
+
+class LIBMULTIMC_EXPORT MinecraftVersionList : public InstVersionList
+{
+ Q_OBJECT
+public:
+ friend class MCVListLoadTask;
+
+ explicit MinecraftVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask();
+ virtual bool isLoaded();
+ virtual const InstVersion *at(int i) const;
+ virtual int count() const;
+ virtual void printToStdOut() const;
+ virtual void sort();
+
+ virtual InstVersion *getLatestStable() const;
+
+ /*!
+ * Gets the main version list instance.
+ */
+ static MinecraftVersionList &getMainList();
+
+
+protected:
+ QList<InstVersion *>m_vlist;
+
+ bool m_loaded;
+
+protected slots:
+ virtual void updateListData(QList<InstVersion *> versions);
+};
+
+class MCVListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit MCVListLoadTask(MinecraftVersionList *vlist);
+ ~MCVListLoadTask();
+
+ virtual void executeTask();
+
+protected:
+ void setSubStatus(const QString msg = "");
+
+ //! Loads versions from Mojang's official version list.
+ bool loadFromVList();
+
+ //! Loads versions from assets.minecraft.net. Any duplicates are ignored.
+ bool loadFromAssets();
+
+ //! Loads versions from MCNostalgia.
+ bool loadMCNostalgia();
+
+ //! Finalizes loading by updating the version list.
+ bool finalize();
+
+ void updateStuff();
+
+ QNetworkAccessManager *netMgr;
+
+ MinecraftVersionList *m_list;
+ QList<InstVersion *> tempList; //! < List of loaded versions
+ QList<InstVersion *> assetsList; //! < List of versions loaded from assets.minecraft.net
+ QList<InstVersion *> mcnList; //! < List of loaded MCNostalgia versions
+
+ MinecraftVersion *m_currentStable;
+
+ bool processedMCVListReply;
+ bool processedAssetsReply;
+ bool processedMCNReply;
+};
+