From 2e0cbf393a5320dbf5448ca44a9b5905314b0be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 29 Jul 2013 00:59:35 +0200 Subject: Massive renaming in the backend folder, all around restructure in the same. --- backend/lists/InstVersionList.cpp | 129 ++++++++ backend/lists/InstVersionList.h | 121 ++++++++ backend/lists/InstanceList.cpp | 232 +++++++++++++++ backend/lists/InstanceList.h | 92 ++++++ backend/lists/LwjglVersionList.cpp | 204 +++++++++++++ backend/lists/LwjglVersionList.h | 132 +++++++++ backend/lists/MinecraftVersionList.cpp | 528 +++++++++++++++++++++++++++++++++ backend/lists/MinecraftVersionList.h | 106 +++++++ 8 files changed, 1544 insertions(+) create mode 100644 backend/lists/InstVersionList.cpp create mode 100644 backend/lists/InstVersionList.h create mode 100644 backend/lists/InstanceList.cpp create mode 100644 backend/lists/InstanceList.h create mode 100644 backend/lists/LwjglVersionList.cpp create mode 100644 backend/lists/LwjglVersionList.h create mode 100644 backend/lists/MinecraftVersionList.cpp create mode 100644 backend/lists/MinecraftVersionList.h (limited to 'backend/lists') 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 +#include +#include + +#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 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 +#include +#include +#include +#include +#include +#include +#include + +#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 & 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 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 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 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 +#include + +#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 & 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 + +#include + +#include + +#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 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 +#include +#include +#include + +#include +#include + +#include "libmmc_config.h" + +class LWJGLVersion; +typedef QSharedPointer 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 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 + +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#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(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 versions) +{ + // First, we populate a temporary list with the copies of the versions. + QList 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, 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 + +#include + +#include + +#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: + QListm_vlist; + + bool m_loaded; + +protected slots: + virtual void updateListData(QList 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 tempList; //! < List of loaded versions + QList assetsList; //! < List of versions loaded from assets.minecraft.net + QList mcnList; //! < List of loaded MCNostalgia versions + + MinecraftVersion *m_currentStable; + + bool processedMCVListReply; + bool processedAssetsReply; + bool processedMCNReply; +}; + -- cgit v1.2.3