diff options
author | Petr Mrázek <peterix@gmail.com> | 2013-07-29 00:59:35 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2013-07-29 00:59:35 +0200 |
commit | 2e0cbf393a5320dbf5448ca44a9b5905314b0be8 (patch) | |
tree | 4baac9cf015ca7b15d83de33c705e0d8d4497d30 /backend/lists/MinecraftVersionList.cpp | |
parent | 8808a8b108b82916eaf30f9aca50cd3ab16af230 (diff) | |
download | MultiMC-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/MinecraftVersionList.cpp')
-rw-r--r-- | backend/lists/MinecraftVersionList.cpp | 528 |
1 files changed, 528 insertions, 0 deletions
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(); + } +} |