From b6d455a02bd338e9dc0faa09d4d8177ecd8d569a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 10 Apr 2016 15:53:05 +0200 Subject: NOISSUE reorganize and document libraries --- api/logic/minecraft/MinecraftVersionList.cpp | 591 +++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 api/logic/minecraft/MinecraftVersionList.cpp (limited to 'api/logic/minecraft/MinecraftVersionList.cpp') diff --git a/api/logic/minecraft/MinecraftVersionList.cpp b/api/logic/minecraft/MinecraftVersionList.cpp new file mode 100644 index 00000000..a5cc3a39 --- /dev/null +++ b/api/logic/minecraft/MinecraftVersionList.cpp @@ -0,0 +1,591 @@ +/* Copyright 2013-2015 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 "Json.h" +#include +#include + +#include "Env.h" +#include "Exception.h" + +#include "MinecraftVersionList.h" +#include "net/URLConstants.h" + +#include "ParseUtils.h" +#include "ProfileUtils.h" +#include "VersionFilterData.h" +#include "onesix/OneSixVersionFormat.h" +#include "MojangVersionFormat.h" +#include + +static const char * localVersionCache = "versions/versions.dat"; + +class MCVListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit MCVListLoadTask(MinecraftVersionList *vlist); + virtual ~MCVListLoadTask() override{}; + + virtual void executeTask() override; + +protected +slots: + void list_downloaded(); + +protected: + QNetworkReply *vlistReply; + MinecraftVersionList *m_list; + MinecraftVersion *m_currentStable; +}; + +class MCVListVersionUpdateTask : public Task +{ + Q_OBJECT + +public: + explicit MCVListVersionUpdateTask(MinecraftVersionList *vlist, std::shared_ptr updatedVersion); + virtual ~MCVListVersionUpdateTask() override{}; + virtual void executeTask() override; + +protected +slots: + void json_downloaded(); + +protected: + NetJobPtr specificVersionDownloadJob; + std::shared_ptr updatedVersion; + MinecraftVersionList *m_list; +}; + +class ListLoadError : public Exception +{ +public: + ListLoadError(QString cause) : Exception(cause) {}; + virtual ~ListLoadError() noexcept + { + } +}; + +MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent) +{ + loadCachedList(); +} + +Task *MinecraftVersionList::getLoadTask() +{ + return new MCVListLoadTask(this); +} + +bool MinecraftVersionList::isLoaded() +{ + return m_loaded; +} + +const BaseVersionPtr MinecraftVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int MinecraftVersionList::count() const +{ + return m_vlist.count(); +} + +static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second) +{ + auto left = std::dynamic_pointer_cast(first); + auto right = std::dynamic_pointer_cast(second); + return left->getReleaseDateTime() > right->getReleaseDateTime(); +} + +void MinecraftVersionList::sortInternal() +{ + qSort(m_vlist.begin(), m_vlist.end(), cmpVersions); +} + +void MinecraftVersionList::loadCachedList() +{ + QFile localIndex(localVersionCache); + if (!localIndex.exists()) + { + return; + } + if (!localIndex.open(QIODevice::ReadOnly)) + { + // FIXME: this is actually a very bad thing! How do we deal with this? + qCritical() << "The minecraft version cache can't be read."; + return; + } + auto data = localIndex.readAll(); + try + { + localIndex.close(); + QJsonDocument jsonDoc = QJsonDocument::fromBinaryData(data); + if (jsonDoc.isNull()) + { + throw ListLoadError(tr("Error reading the version list.")); + } + loadList(jsonDoc, Local); + } + catch (Exception &e) + { + // the cache has gone bad for some reason... flush it. + qCritical() << "The minecraft version cache is corrupted. Flushing cache."; + localIndex.remove(); + return; + } + m_hasLocalIndex = true; +} + +void MinecraftVersionList::loadList(QJsonDocument jsonDoc, VersionSource source) +{ + qDebug() << "Loading" << ((source == Remote) ? "remote" : "local") << "version list."; + + if (!jsonDoc.isObject()) + { + throw ListLoadError(tr("Error parsing version list JSON: jsonDoc is not an object")); + } + + QJsonObject root = jsonDoc.object(); + + try + { + QJsonObject latest = Json::requireObject(root.value("latest")); + m_latestReleaseID = Json::requireString(latest.value("release")); + m_latestSnapshotID = Json::requireString(latest.value("snapshot")); + } + catch (Exception &err) + { + qCritical() + << tr("Error parsing version list JSON: couldn't determine latest versions"); + } + + // Now, get the array of versions. + if (!root.value("versions").isArray()) + { + throw ListLoadError(tr("Error parsing version list JSON: version list object is " + "missing 'versions' array")); + } + QJsonArray versions = root.value("versions").toArray(); + + QList tempList; + for (auto version : versions) + { + // Load the version info. + if (!version.isObject()) + { + qCritical() << "Error while parsing version list : invalid JSON structure"; + continue; + } + + QJsonObject versionObj = version.toObject(); + QString versionID = versionObj.value("id").toString(""); + if (versionID.isEmpty()) + { + qCritical() << "Error while parsing version : version ID is missing"; + continue; + } + + if (g_VersionFilterData.legacyBlacklist.contains(versionID)) + { + qWarning() << "Blacklisted legacy version ignored: " << versionID; + continue; + } + + // Now, we construct the version object and add it to the list. + std::shared_ptr mcVersion(new MinecraftVersion()); + mcVersion->m_version = versionID; + + mcVersion->m_releaseTime = timeFromS3Time(versionObj.value("releaseTime").toString("")); + mcVersion->m_updateTime = timeFromS3Time(versionObj.value("time").toString("")); + + // depends on where we load the version from -- network request or local file? + mcVersion->m_versionSource = source; + mcVersion->m_versionFileURL = versionObj.value("url").toString(""); + QString versionTypeStr = versionObj.value("type").toString(""); + if (versionTypeStr.isEmpty()) + { + qCritical() << "Ignoring" << versionID + << "because it doesn't have the version type set."; + continue; + } + // OneSix or Legacy. use filter to determine type + if (versionTypeStr == "release") + { + } + else if (versionTypeStr == "snapshot") // It's a snapshot... yay + { + } + else if (versionTypeStr == "old_alpha") + { + } + else if (versionTypeStr == "old_beta") + { + } + else + { + qCritical() << "Ignoring" << versionID + << "because it has an invalid version type."; + continue; + } + mcVersion->m_type = versionTypeStr; + qDebug() << "Loaded version" << versionID << "from" + << ((source == Remote) ? "remote" : "local") << "version list."; + tempList.append(mcVersion); + } + updateListData(tempList); + if(source == Remote) + { + m_loaded = true; + } +} + +void MinecraftVersionList::sortVersions() +{ + beginResetModel(); + sortInternal(); + endResetModel(); +} + +QVariant MinecraftVersionList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(m_vlist[index.row()]); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case RecommendedRole: + return version->descriptor() == m_latestReleaseID; + + case LatestRole: + { + if(version->descriptor() != m_latestSnapshotID) + return false; + MinecraftVersionPtr latestRelease = std::dynamic_pointer_cast(getLatestStable()); + /* + if(latestRelease && latestRelease->m_releaseTime > version->m_releaseTime) + { + return false; + } + */ + return true; + } + + case TypeRole: + return version->typeString(); + + default: + return QVariant(); + } +} + +BaseVersionList::RoleList MinecraftVersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, RecommendedRole, LatestRole, TypeRole}; +} + +BaseVersionPtr MinecraftVersionList::getLatestStable() const +{ + if(m_lookup.contains(m_latestReleaseID)) + return m_lookup[m_latestReleaseID]; + return BaseVersionPtr(); +} + +BaseVersionPtr MinecraftVersionList::getRecommended() const +{ + return getLatestStable(); +} + +void MinecraftVersionList::updateListData(QList versions) +{ + beginResetModel(); + for (auto version : versions) + { + auto descr = version->descriptor(); + + if (!m_lookup.contains(descr)) + { + m_lookup[version->descriptor()] = version; + m_vlist.append(version); + continue; + } + auto orig = std::dynamic_pointer_cast(m_lookup[descr]); + auto added = std::dynamic_pointer_cast(version); + // updateListData is called after Mojang list loads. those can be local or remote + // remote comes always after local + // any other options are ignored + if (orig->m_versionSource != Local || added->m_versionSource != Remote) + { + continue; + } + // alright, it's an update. put it inside the original, for further processing. + orig->upstreamUpdate = added; + } + sortInternal(); + endResetModel(); +} + +MCVListLoadTask::MCVListLoadTask(MinecraftVersionList *vlist) +{ + m_list = vlist; + m_currentStable = NULL; + vlistReply = nullptr; +} + +void MCVListLoadTask::executeTask() +{ + setStatus(tr("Loading instance version list...")); + auto worker = ENV.qnam(); + vlistReply = worker->get(QNetworkRequest(QUrl("https://launchermeta.mojang.com/mc/game/version_manifest.json"))); + connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded())); +} + +void MCVListLoadTask::list_downloaded() +{ + if (vlistReply->error() != QNetworkReply::NoError) + { + vlistReply->deleteLater(); + emitFailed("Failed to load Minecraft main version list" + vlistReply->errorString()); + return; + } + + auto data = vlistReply->readAll(); + vlistReply->deleteLater(); + try + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + throw ListLoadError( + tr("Error parsing version list JSON: %1").arg(jsonError.errorString())); + } + m_list->loadList(jsonDoc, Remote); + } + catch (Exception &e) + { + emitFailed(e.cause()); + return; + } + + emitSucceeded(); + return; +} + +MCVListVersionUpdateTask::MCVListVersionUpdateTask(MinecraftVersionList *vlist, std::shared_ptr updatedVersion) + : Task() +{ + m_list = vlist; + this->updatedVersion = updatedVersion; +} + +void MCVListVersionUpdateTask::executeTask() +{ + auto job = new NetJob("Version index"); + job->addNetAction(ByteArrayDownload::make(QUrl(updatedVersion->getUrl()))); + specificVersionDownloadJob.reset(job); + connect(specificVersionDownloadJob.get(), SIGNAL(succeeded()), SLOT(json_downloaded())); + connect(specificVersionDownloadJob.get(), SIGNAL(failed(QString)), SIGNAL(failed(QString))); + connect(specificVersionDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + specificVersionDownloadJob->start(); +} + +void MCVListVersionUpdateTask::json_downloaded() +{ + NetActionPtr DlJob = specificVersionDownloadJob->first(); + auto data = std::dynamic_pointer_cast(DlJob)->m_data; + specificVersionDownloadJob.reset(); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed(tr("The download version file is not valid.")); + return; + } + VersionFilePtr file; + try + { + file = MojangVersionFormat::versionFileFromJson(jsonDoc, "net.minecraft.json"); + } + catch (Exception &e) + { + emitFailed(tr("Couldn't process version file: %1").arg(e.cause())); + return; + } + + // Strip LWJGL from the version file. We use our own. + ProfileUtils::removeLwjglFromPatch(file); + + file->fileId = "net.minecraft"; + + // now dump the file to disk + auto doc = OneSixVersionFormat::versionFileToJson(file, false); + auto newdata = doc.toBinaryData(); + auto id = updatedVersion->descriptor(); + QString targetPath = "versions/" + id + "/" + id + ".dat"; + FS::ensureFilePathExists(targetPath); + QSaveFile vfile1(targetPath); + if (!vfile1.open(QIODevice::Truncate | QIODevice::WriteOnly)) + { + emitFailed(tr("Can't open %1 for writing.").arg(targetPath)); + return; + } + qint64 actual = 0; + if ((actual = vfile1.write(newdata)) != newdata.size()) + { + emitFailed(tr("Failed to write into %1. Written %2 out of %3.") + .arg(targetPath) + .arg(actual) + .arg(newdata.size())); + return; + } + if (!vfile1.commit()) + { + emitFailed(tr("Can't commit changes to %1").arg(targetPath)); + return; + } + + m_list->finalizeUpdate(id); + emitSucceeded(); +} + +std::shared_ptr MinecraftVersionList::createUpdateTask(QString version) +{ + auto iter = m_lookup.find(version); + if(iter == m_lookup.end()) + return nullptr; + + auto mcversion = std::dynamic_pointer_cast(*iter); + if(!mcversion) + { + return nullptr; + } + + return std::shared_ptr(new MCVListVersionUpdateTask(this, mcversion)); +} + +void MinecraftVersionList::saveCachedList() +{ + // FIXME: throw. + if (!FS::ensureFilePathExists(localVersionCache)) + return; + QSaveFile tfile(localVersionCache); + if (!tfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) + return; + QJsonObject toplevel; + QJsonArray entriesArr; + for (auto version : m_vlist) + { + auto mcversion = std::dynamic_pointer_cast(version); + // do not save the remote versions. + if (mcversion->m_versionSource != Local) + continue; + QJsonObject entryObj; + + entryObj.insert("id", mcversion->descriptor()); + entryObj.insert("version", mcversion->descriptor()); + entryObj.insert("time", timeToS3Time(mcversion->m_updateTime)); + entryObj.insert("releaseTime", timeToS3Time(mcversion->m_releaseTime)); + entryObj.insert("url", mcversion->m_versionFileURL); + entryObj.insert("type", mcversion->m_type); + entriesArr.append(entryObj); + } + toplevel.insert("versions", entriesArr); + + { + bool someLatest = false; + QJsonObject latestObj; + if(!m_latestReleaseID.isNull()) + { + latestObj.insert("release", m_latestReleaseID); + someLatest = true; + } + if(!m_latestSnapshotID.isNull()) + { + latestObj.insert("snapshot", m_latestSnapshotID); + someLatest = true; + } + if(someLatest) + { + toplevel.insert("latest", latestObj); + } + } + + QJsonDocument doc(toplevel); + QByteArray jsonData = doc.toBinaryData(); + qint64 result = tfile.write(jsonData); + if (result == -1) + return; + if (result != jsonData.size()) + return; + tfile.commit(); +} + +void MinecraftVersionList::finalizeUpdate(QString version) +{ + int idx = -1; + for (int i = 0; i < m_vlist.size(); i++) + { + if (version == m_vlist[i]->descriptor()) + { + idx = i; + break; + } + } + if (idx == -1) + { + return; + } + + auto updatedVersion = std::dynamic_pointer_cast(m_vlist[idx]); + + // if we have an update for the version, replace it, make the update local + if (updatedVersion->upstreamUpdate) + { + auto updatedWith = updatedVersion->upstreamUpdate; + updatedWith->m_versionSource = Local; + m_vlist[idx] = updatedWith; + m_lookup[version] = updatedWith; + } + else + { + // otherwise, just set the version as local; + updatedVersion->m_versionSource = Local; + } + + dataChanged(index(idx), index(idx)); + + saveCachedList(); +} + +#include "MinecraftVersionList.moc" -- cgit v1.2.3