diff options
Diffstat (limited to 'logic/minecraft/forge')
-rw-r--r-- | logic/minecraft/forge/ForgeInstaller.cpp | 442 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeInstaller.h | 52 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeMirror.h | 10 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeMirrors.cpp | 118 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeMirrors.h | 61 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeVersion.cpp | 55 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeVersion.h | 42 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeVersionList.cpp | 450 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeVersionList.h | 90 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeXzDownload.cpp | 389 | ||||
-rw-r--r-- | logic/minecraft/forge/ForgeXzDownload.h | 66 | ||||
-rw-r--r-- | logic/minecraft/forge/LegacyForge.cpp | 56 | ||||
-rw-r--r-- | logic/minecraft/forge/LegacyForge.h | 25 |
13 files changed, 1856 insertions, 0 deletions
diff --git a/logic/minecraft/forge/ForgeInstaller.cpp b/logic/minecraft/forge/ForgeInstaller.cpp new file mode 100644 index 00000000..a9283e5d --- /dev/null +++ b/logic/minecraft/forge/ForgeInstaller.cpp @@ -0,0 +1,442 @@ +/* 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 "ForgeInstaller.h" +#include "ForgeVersionList.h" + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/GradleSpecifier.h" +#include "net/HttpMetaCache.h" +#include "tasks/Task.h" +#include "minecraft/onesix/OneSixInstance.h" +#include "minecraft/VersionFilterData.h" +#include "Env.h" +#include "Exception.h" +#include <FileSystem.h> + +#include <quazip.h> +#include <quazipfile.h> +#include <QStringList> +#include <QRegularExpression> +#include <QRegularExpressionMatch> + +#include <QJsonDocument> +#include <QJsonArray> +#include <QSaveFile> +#include <QCryptographicHash> + +ForgeInstaller::ForgeInstaller() : BaseInstaller() +{ +} + +void ForgeInstaller::prepare(const QString &filename, const QString &universalUrl) +{ + std::shared_ptr<MinecraftProfile> newVersion; + m_universal_url = universalUrl; + + QuaZip zip(filename); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + // read the install profile + if (!zip.setCurrentFile("install_profile.json")) + return; + + QJsonParseError jsonError; + if (!file.open(QIODevice::ReadOnly)) + return; + QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &jsonError); + file.close(); + if (jsonError.error != QJsonParseError::NoError) + return; + + if (!jsonDoc.isObject()) + return; + + QJsonObject root = jsonDoc.object(); + + auto installVal = root.value("install"); + auto versionInfoVal = root.value("versionInfo"); + if (!installVal.isObject() || !versionInfoVal.isObject()) + return; + + // read the forge version info + { + newVersion = MinecraftProfile::fromJson(versionInfoVal.toObject()); + if (!newVersion) + return; + } + + QJsonObject installObj = installVal.toObject(); + QString libraryName = installObj.value("path").toString(); + internalPath = installObj.value("filePath").toString(); + m_forgeVersionString = installObj.value("version").toString().remove("Forge").trimmed(); + + // where do we put the library? decode the mojang path + GradleSpecifier lib(libraryName); + + auto cacheentry = ENV.metacache()->resolveEntry("libraries", lib.toPath()); + finalPath = "libraries/" + lib.toPath(); + if (!FS::ensureFilePathExists(finalPath)) + return; + + if (!zip.setCurrentFile(internalPath)) + return; + if (!file.open(QIODevice::ReadOnly)) + return; + { + QByteArray data = file.readAll(); + // extract file + QSaveFile extraction(finalPath); + if (!extraction.open(QIODevice::WriteOnly)) + return; + if (extraction.write(data) != data.size()) + return; + if (!extraction.commit()) + return; + QCryptographicHash md5sum(QCryptographicHash::Md5); + md5sum.addData(data); + + cacheentry->stale = false; + cacheentry->md5sum = md5sum.result().toHex().constData(); + ENV.metacache()->updateEntry(cacheentry); + } + file.close(); + + m_forge_json = newVersion; + m_forge_json->id = installObj.value("minecraft").toString(); +} + +bool ForgeInstaller::add(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + + QJsonObject obj; + obj.insert("order", 5); + + if (!m_forge_json) + return false; + int sliding_insert_window = 0; + { + QJsonArray librariesPlus; + // A blacklist + QSet<QString> blacklist{"authlib", "realms"}; + // + QList<QString> xzlist{"org.scala-lang", "com.typesafe"}; + // for each library in the version we are adding (except for the blacklisted) + for (auto lib : m_forge_json->libraries) + { + QString libName = lib->artifactId(); + QString rawName = lib->rawName(); + + // ignore lwjgl libraries. + if (g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + continue; + // ignore other blacklisted (realms, authlib) + if (blacklist.contains(libName)) + continue; + + // WARNING: This could actually break. + // if this is the actual forge lib, set an absolute url for the download + if (m_forge_version->type == ForgeVersion::Gradle) + { + if (libName == "forge") + { + lib->setClassifier("universal"); + } + else if (libName == "minecraftforge") + { + QString forgeCoord("net.minecraftforge:forge:%1:universal"); + // using insane form of the MC version... + QString longVersion = + m_forge_version->mcver + "-" + m_forge_version->jobbuildver; + GradleSpecifier spec(forgeCoord.arg(longVersion)); + lib->setRawName(spec); + } + } + else + { + if (libName.contains("minecraftforge")) + { + lib->setAbsoluteUrl(m_universal_url); + } + } + + // WARNING: This could actually break. + // mark bad libraries based on the xzlist above + for (auto entry : xzlist) + { + qDebug() << "Testing " << rawName << " : " << entry; + if (rawName.startsWith(entry)) + { + lib->setHint("forge-pack-xz"); + break; + } + } + + QJsonObject libObj = lib->toJson(); + + bool found = false; + bool equals = false; + // find an entry that matches this one + for (auto tolib : to->getMinecraftProfile()->vanillaLibraries) + { + if (tolib->artifactId() != libName) + continue; + found = true; + if (tolib->toJson() == libObj) + { + equals = true; + } + // replace lib + libObj.insert("insert", QString("replace")); + break; + } + if (equals) + { + continue; + } + if (!found) + { + // add lib + libObj.insert("insert", QString("prepend")); + if (lib->artifactId() == "minecraftforge" || lib->artifactId() == "forge") + { + libObj.insert("MMC-depend", QString("hard")); + } + sliding_insert_window++; + } + librariesPlus.prepend(libObj); + } + obj.insert("+libraries", librariesPlus); + obj.insert("mainClass", m_forge_json->mainClass); + QString args = m_forge_json->minecraftArguments; + QStringList tweakers; + { + QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)"); + QRegularExpressionMatch match = expression.match(args); + while (match.hasMatch()) + { + tweakers.append(match.captured(1)); + args.remove(match.capturedStart(), match.capturedLength()); + match = expression.match(args); + } + } + if (!args.isEmpty() && args != to->getMinecraftProfile()->vanillaMinecraftArguments) + { + obj.insert("minecraftArguments", args); + } + if (!tweakers.isEmpty()) + { + obj.insert("+tweakers", QJsonArray::fromStringList(tweakers)); + } + if (!m_forge_json->processArguments.isEmpty() && + m_forge_json->processArguments != to->getMinecraftProfile()->vanillaProcessArguments) + { + obj.insert("processArguments", m_forge_json->processArguments); + } + } + + obj.insert("name", QString("Forge")); + obj.insert("fileId", id()); + obj.insert("version", m_forgeVersionString); + obj.insert("mcVersion", to->intendedVersionId()); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(QJsonDocument(obj).toJson()); + file.close(); + + return true; +} + +bool ForgeInstaller::addLegacy(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + auto entry = ENV.metacache()->resolveEntry("minecraftforge", m_forge_version->filename()); + finalPath = FS::PathCombine(to->jarModsDir(), m_forge_version->filename()); + if (!FS::ensureFilePathExists(finalPath)) + { + return false; + } + if (!QFile::copy(entry->getFullPath(), finalPath)) + { + return false; + } + QJsonObject obj; + obj.insert("order", 5); + { + QJsonArray jarmodsPlus; + { + QJsonObject libObj; + libObj.insert("name", m_forge_version->universal_filename); + jarmodsPlus.append(libObj); + } + obj.insert("+jarMods", jarmodsPlus); + } + + obj.insert("name", QString("Forge")); + obj.insert("fileId", id()); + obj.insert("version", m_forge_version->jobbuildver); + obj.insert("mcVersion", to->intendedVersionId()); + if (g_VersionFilterData.fmlLibsMapping.contains(m_forge_version->mcver)) + { + QJsonArray traitsPlus; + traitsPlus.append(QString("legacyFML")); + obj.insert("+traits", traitsPlus); + } + auto fullversion = to->getMinecraftProfile(); + fullversion->remove("net.minecraftforge"); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(QJsonDocument(obj).toJson()); + file.close(); + return true; +} + +class ForgeInstallTask : public Task +{ + Q_OBJECT +public: + ForgeInstallTask(ForgeInstaller *installer, OneSixInstance *instance, + BaseVersionPtr version, QObject *parent = 0) + : Task(parent), m_installer(installer), m_instance(instance), m_version(version) + { + } + +protected: + void executeTask() override + { + setStatus(tr("Installing Forge...")); + ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast<ForgeVersion>(m_version); + if (!forgeVersion) + { + emitFailed(tr("Unknown error occured")); + return; + } + prepare(forgeVersion); + } + void prepare(ForgeVersionPtr forgeVersion) + { + auto entry = ENV.metacache()->resolveEntry("minecraftforge", forgeVersion->filename()); + auto installFunction = [this, entry, forgeVersion]() + { + if (!install(entry, forgeVersion)) + { + qCritical() << "Failure installing Forge"; + emitFailed(tr("Failure to install Forge")); + } + else + { + reload(); + } + }; + + /* + * HACK IF the local non-stale file is too small, mark is as stale + * + * This fixes some problems with bad files acquired because of unhandled HTTP redirects + * in old versions of MultiMC. + */ + if (!entry->stale) + { + QFileInfo localFile(entry->getFullPath()); + if (localFile.size() <= 0x4000) + { + entry->stale = true; + } + } + + if (entry->stale) + { + NetJob *fjob = new NetJob("Forge download"); + fjob->addNetAction(CacheDownload::make(forgeVersion->url(), entry)); + connect(fjob, &NetJob::progress, this, &Task::setProgress); + connect(fjob, &NetJob::status, this, &Task::setStatus); + connect(fjob, &NetJob::failed, [this](QString reason) + { emitFailed(tr("Failure to download Forge:\n%1").arg(reason)); }); + connect(fjob, &NetJob::succeeded, installFunction); + fjob->start(); + } + else + { + installFunction(); + } + } + bool install(const std::shared_ptr<MetaEntry> &entry, const ForgeVersionPtr &forgeVersion) + { + if (forgeVersion->usesInstaller()) + { + QString forgePath = entry->getFullPath(); + m_installer->prepare(forgePath, forgeVersion->universal_url); + return m_installer->add(m_instance); + } + else + return m_installer->addLegacy(m_instance); + } + void reload() + { + try + { + m_instance->reloadProfile(); + emitSucceeded(); + } + catch (Exception &e) + { + emitFailed(e.cause()); + } + catch (...) + { + emitFailed(tr("Failed to load the version description file for reasons unknown.")); + } + } + +private: + ForgeInstaller *m_installer; + OneSixInstance *m_instance; + BaseVersionPtr m_version; +}; + +Task *ForgeInstaller::createInstallTask(OneSixInstance *instance, + BaseVersionPtr version, QObject *parent) +{ + if (!version) + { + return nullptr; + } + m_forge_version = std::dynamic_pointer_cast<ForgeVersion>(version); + return new ForgeInstallTask(this, instance, version, parent); +} + +#include "ForgeInstaller.moc" diff --git a/logic/minecraft/forge/ForgeInstaller.h b/logic/minecraft/forge/ForgeInstaller.h new file mode 100644 index 00000000..0de762b6 --- /dev/null +++ b/logic/minecraft/forge/ForgeInstaller.h @@ -0,0 +1,52 @@ +/* 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. + */ + +#pragma once + +#include "BaseInstaller.h" + +#include <QString> +#include <memory> + +#include "multimc_logic_export.h" + +class MinecraftProfile; +class ForgeInstallTask; +struct ForgeVersion; + +class MULTIMC_LOGIC_EXPORT ForgeInstaller : public BaseInstaller +{ + friend class ForgeInstallTask; +public: + ForgeInstaller(); + virtual ~ForgeInstaller(){} + virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override; + virtual QString id() const override { return "net.minecraftforge"; } + +protected: + void prepare(const QString &filename, const QString &universalUrl); + bool add(OneSixInstance *to) override; + bool addLegacy(OneSixInstance *to); + +private: + // the parsed version json, read from the installer + std::shared_ptr<MinecraftProfile> m_forge_json; + // the actual forge version + std::shared_ptr<ForgeVersion> m_forge_version; + QString internalPath; + QString finalPath; + QString m_forgeVersionString; + QString m_universal_url; +}; diff --git a/logic/minecraft/forge/ForgeMirror.h b/logic/minecraft/forge/ForgeMirror.h new file mode 100644 index 00000000..2518dffe --- /dev/null +++ b/logic/minecraft/forge/ForgeMirror.h @@ -0,0 +1,10 @@ +#pragma once +#include <QString> + +struct ForgeMirror +{ + QString name; + QString logo_url; + QString website_url; + QString mirror_url; +};
\ No newline at end of file diff --git a/logic/minecraft/forge/ForgeMirrors.cpp b/logic/minecraft/forge/ForgeMirrors.cpp new file mode 100644 index 00000000..a2fc2c62 --- /dev/null +++ b/logic/minecraft/forge/ForgeMirrors.cpp @@ -0,0 +1,118 @@ +#include "Env.h" +#include "ForgeMirrors.h" +#include <QDebug> +#include <algorithm> +#include <random> + +ForgeMirrors::ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job, + QString mirrorlist) +{ + m_libs = libs; + m_parent_job = parent_job; + m_url = QUrl(mirrorlist); + m_status = Job_NotStarted; +} + +void ForgeMirrors::start() +{ + qDebug() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + auto worker = ENV.qnam(); + QNetworkReply *rep = worker->get(request); + + m_reply.reset(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); +} + +void ForgeMirrors::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Error getting URL:" << m_url.toString().toLocal8Bit() + << "Network error: " << error; + m_status = Job_Failed; +} + +void ForgeMirrors::downloadFinished() +{ + // if the download succeeded + if (m_status != Job_Failed) + { + // nothing went wrong... ? + parseMirrorList(); + return; + } + // else the download failed, we use a fixed list + else + { + m_status = Job_Finished; + m_reply.reset(); + deferToFixedList(); + return; + } +} + +void ForgeMirrors::deferToFixedList() +{ + m_mirrors.clear(); + m_mirrors.append( + {"Minecraft Forge", "http://files.minecraftforge.net/forge_logo.png", + "http://files.minecraftforge.net/", "http://files.minecraftforge.net/maven/"}); + m_mirrors.append({"Creeper Host", + "http://files.minecraftforge.net/forge_logo.png", + "https://www.creeperhost.net/link.php?id=1", + "http://new.creeperrepo.net/forge/maven/"}); + injectDownloads(); + emit succeeded(m_index_within_job); +} + +void ForgeMirrors::parseMirrorList() +{ + m_status = Job_Finished; + auto data = m_reply->readAll(); + m_reply.reset(); + auto dataLines = data.split('\n'); + for(auto line: dataLines) + { + auto elements = line.split('!'); + if (elements.size() == 4) + { + m_mirrors.append({elements[0],elements[1],elements[2],elements[3]}); + } + } + if(!m_mirrors.size()) + deferToFixedList(); + injectDownloads(); + emit succeeded(m_index_within_job); +} + +void ForgeMirrors::injectDownloads() +{ + // shuffle the mirrors randomly + std::random_device rd; + std::mt19937 rng(rd()); + std::shuffle(m_mirrors.begin(), m_mirrors.end(), rng); + + // tell parent to download the libs + for(auto lib: m_libs) + { + lib->setMirrors(m_mirrors); + m_parent_job->addNetAction(lib); + } +} + +void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} + +void ForgeMirrors::downloadReadyRead() +{ +} diff --git a/logic/minecraft/forge/ForgeMirrors.h b/logic/minecraft/forge/ForgeMirrors.h new file mode 100644 index 00000000..0312829b --- /dev/null +++ b/logic/minecraft/forge/ForgeMirrors.h @@ -0,0 +1,61 @@ +/* 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. + */ + +#pragma once + +#include "ForgeXzDownload.h" + +#include "net/NetAction.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" + +#include <QFile> +#include <QTemporaryFile> + +typedef std::shared_ptr<class ForgeMirrors> ForgeMirrorsPtr; + +class ForgeMirrors : public NetAction +{ + Q_OBJECT +public: + QList<ForgeXzDownloadPtr> m_libs; + NetJobPtr m_parent_job; + QList<ForgeMirror> m_mirrors; + +public: + explicit ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job, + QString mirrorlist); + static ForgeMirrorsPtr make(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job, + QString mirrorlist) + { + return ForgeMirrorsPtr(new ForgeMirrors(libs, parent_job, mirrorlist)); + } + virtual ~ForgeMirrors(){}; +protected +slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead(); + +private: + void parseMirrorList(); + void deferToFixedList(); + void injectDownloads(); + +public +slots: + virtual void start(); +}; diff --git a/logic/minecraft/forge/ForgeVersion.cpp b/logic/minecraft/forge/ForgeVersion.cpp new file mode 100644 index 00000000..b859a28c --- /dev/null +++ b/logic/minecraft/forge/ForgeVersion.cpp @@ -0,0 +1,55 @@ +#include "ForgeVersion.h" +#include "minecraft/VersionFilterData.h" +#include <QObject> + +QString ForgeVersion::name() +{ + return "Forge " + jobbuildver; +} + +QString ForgeVersion::descriptor() +{ + return universal_filename; +} + +QString ForgeVersion::typeString() const +{ + if (is_recommended) + return QObject::tr("Recommended"); + return QString(); +} + +bool ForgeVersion::operator<(BaseVersion &a) +{ + ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a); + if (!pa) + return true; + return m_buildnr < pa->m_buildnr; +} + +bool ForgeVersion::operator>(BaseVersion &a) +{ + ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a); + if (!pa) + return false; + return m_buildnr > pa->m_buildnr; +} + +bool ForgeVersion::usesInstaller() +{ + if(installer_url.isEmpty()) + return false; + if(g_VersionFilterData.forgeInstallerBlacklist.contains(mcver)) + return false; + return true; +} + +QString ForgeVersion::filename() +{ + return usesInstaller() ? installer_filename : universal_filename; +} + +QString ForgeVersion::url() +{ + return usesInstaller() ? installer_url : universal_url; +} diff --git a/logic/minecraft/forge/ForgeVersion.h b/logic/minecraft/forge/ForgeVersion.h new file mode 100644 index 00000000..e77d32f1 --- /dev/null +++ b/logic/minecraft/forge/ForgeVersion.h @@ -0,0 +1,42 @@ +#pragma once +#include <QString> +#include <memory> +#include "BaseVersion.h" + +struct ForgeVersion; +typedef std::shared_ptr<ForgeVersion> ForgeVersionPtr; + +struct ForgeVersion : public BaseVersion +{ + virtual QString descriptor() override; + virtual QString name() override; + virtual QString typeString() const override; + virtual bool operator<(BaseVersion &a) override; + virtual bool operator>(BaseVersion &a) override; + + QString filename(); + QString url(); + + enum + { + Invalid, + Legacy, + Gradle + } type = Invalid; + + bool usesInstaller(); + + int m_buildnr = 0; + QString branch; + QString universal_url; + QString changelog_url; + QString installer_url; + QString jobbuildver; + QString mcver; + QString mcver_sane; + QString universal_filename; + QString installer_filename; + bool is_recommended = false; +}; + +Q_DECLARE_METATYPE(ForgeVersionPtr) diff --git a/logic/minecraft/forge/ForgeVersionList.cpp b/logic/minecraft/forge/ForgeVersionList.cpp new file mode 100644 index 00000000..9b418310 --- /dev/null +++ b/logic/minecraft/forge/ForgeVersionList.cpp @@ -0,0 +1,450 @@ +/* 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 "ForgeVersionList.h" +#include "ForgeVersion.h" + +#include "net/NetJob.h" +#include "net/URLConstants.h" +#include "Env.h" + +#include <QtNetwork> +#include <QtXml> +#include <QRegExp> + +#include <QDebug> + +ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent) +{ +} + +Task *ForgeVersionList::getLoadTask() +{ + return new ForgeListLoadTask(this); +} + +bool ForgeVersionList::isLoaded() +{ + return m_loaded; +} + +const BaseVersionPtr ForgeVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int ForgeVersionList::count() const +{ + return m_vlist.count(); +} + +int ForgeVersionList::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ForgeVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast<ForgeVersion>(m_vlist[index.row()]); + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(m_vlist[index.row()]); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case ParentGameVersionRole: + return version->mcver_sane; + + case RecommendedRole: + return version->is_recommended; + + case BranchRole: + return version->branch; + + default: + return QVariant(); + } +} + +QList<BaseVersionList::ModelRoles> ForgeVersionList::providesRoles() +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole}; +} + +BaseVersionPtr ForgeVersionList::getLatestStable() const +{ + return BaseVersionPtr(); +} + +void ForgeVersionList::updateListData(QList<BaseVersionPtr> versions) +{ + beginResetModel(); + m_vlist = versions; + m_loaded = true; + endResetModel(); + // NOW SORT!! + // sort(); +} + +void ForgeVersionList::sortVersions() +{ + // NO-OP for now +} + +ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task() +{ + m_list = vlist; +} + +void ForgeListLoadTask::executeTask() +{ + setStatus(tr("Fetching Forge version lists...")); + auto job = new NetJob("Version index"); + // we do not care if the version is stale or not. + auto forgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "list.json"); + auto gradleForgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "json"); + + // verify by poking the server. + forgeListEntry->stale = true; + gradleForgeListEntry->stale = true; + + job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL), + forgeListEntry)); + job->addNetAction(gradleListDownload = CacheDownload::make( + QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry)); + + connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed())); + connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed())); + + listJob.reset(job); + connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded())); + connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + listJob->start(); +} + +bool ForgeListLoadTask::abort() +{ + return listJob->abort(); +} + +bool ForgeListLoadTask::parseForgeList(QList<BaseVersionPtr> &out) +{ + QByteArray data; + { + auto dlJob = listDownload; + auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Now, get the array of versions. + if (!root.value("builds").isArray()) + { + emitFailed( + "Error parsing version list JSON: version list object is missing 'builds' array"); + return false; + } + QJsonArray builds = root.value("builds").toArray(); + + for (int i = 0; i < builds.count(); i++) + { + // Load the version info. + if (!builds[i].isObject()) + { + // FIXME: log this somewhere + continue; + } + QJsonObject obj = builds[i].toObject(); + int build_nr = obj.value("build").toDouble(0); + if (!build_nr) + continue; + QJsonArray files = obj.value("files").toArray(); + QString url, jobbuildver, mcver, buildtype, universal_filename; + QString changelog_url, installer_url; + QString installer_filename; + bool valid = false; + for (int j = 0; j < files.count(); j++) + { + if (!files[j].isObject()) + { + continue; + } + QJsonObject file = files[j].toObject(); + buildtype = file.value("buildtype").toString(); + if ((buildtype == "client" || buildtype == "universal") && !valid) + { + mcver = file.value("mcver").toString(); + url = file.value("url").toString(); + jobbuildver = file.value("jobbuildver").toString(); + int lastSlash = url.lastIndexOf('/'); + universal_filename = url.mid(lastSlash + 1); + valid = true; + } + else if (buildtype == "changelog") + { + QString ext = file.value("ext").toString(); + if (ext.isEmpty()) + { + continue; + } + changelog_url = file.value("url").toString(); + } + else if (buildtype == "installer") + { + installer_url = file.value("url").toString(); + int lastSlash = installer_url.lastIndexOf('/'); + installer_filename = installer_url.mid(lastSlash + 1); + } + } + if (valid) + { + // Now, we construct the version object and add it to the list. + std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion()); + fVersion->universal_url = url; + fVersion->changelog_url = changelog_url; + fVersion->installer_url = installer_url; + fVersion->jobbuildver = jobbuildver; + fVersion->mcver = fVersion->mcver_sane = mcver; + fVersion->installer_filename = installer_filename; + fVersion->universal_filename = universal_filename; + fVersion->m_buildnr = build_nr; + fVersion->type = ForgeVersion::Legacy; + out.append(fVersion); + } + } + + return true; +} + +bool ForgeListLoadTask::parseForgeGradleList(QList<BaseVersionPtr> &out) +{ + QMap<int, std::shared_ptr<ForgeVersion>> lookup; + QByteArray data; + { + auto dlJob = gradleListDownload; + auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing gradle version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // we probably could hard code these, but it might still be worth doing it this way + const QString webpath = root.value("webpath").toString(); + const QString artifact = root.value("artifact").toString(); + + QJsonObject numbers = root.value("number").toObject(); + for (auto it = numbers.begin(); it != numbers.end(); ++it) + { + QJsonObject number = it.value().toObject(); + std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion()); + fVersion->m_buildnr = number.value("build").toDouble(); + if(fVersion->m_buildnr >= 953 && fVersion->m_buildnr <= 965) + { + qDebug() << fVersion->m_buildnr; + } + fVersion->jobbuildver = number.value("version").toString(); + fVersion->branch = number.value("branch").toString(""); + fVersion->mcver = number.value("mcversion").toString(); + fVersion->universal_filename = ""; + fVersion->installer_filename = ""; + // HACK: here, we fix the minecraft version used by forge. + // HACK: this will inevitably break (later) + // FIXME: replace with a dictionary + fVersion->mcver_sane = fVersion->mcver; + fVersion->mcver_sane.replace("_pre", "-pre"); + + QString universal_filename, installer_filename; + QJsonArray files = number.value("files").toArray(); + for (auto fIt = files.begin(); fIt != files.end(); ++fIt) + { + // TODO with gradle we also get checksums, use them + QJsonArray file = (*fIt).toArray(); + if (file.size() < 3) + { + continue; + } + + QString extension = file.at(0).toString(); + QString part = file.at(1).toString(); + QString checksum = file.at(2).toString(); + + // insane form of mcver is used here + QString longVersion = fVersion->mcver + "-" + fVersion->jobbuildver; + if (!fVersion->branch.isEmpty()) + { + longVersion = longVersion + "-" + fVersion->branch; + } + QString filename = artifact + "-" + longVersion + "-" + part + "." + extension; + + QString url = QString("%1/%2/%3") + .arg(webpath) + .arg(longVersion) + .arg(filename); + + if (part == "installer") + { + fVersion->installer_url = url; + installer_filename = filename; + } + else if (part == "universal") + { + fVersion->universal_url = url; + universal_filename = filename; + } + else if (part == "changelog") + { + fVersion->changelog_url = url; + } + } + if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty()) + { + continue; + } + fVersion->universal_filename = universal_filename; + fVersion->installer_filename = installer_filename; + fVersion->type = ForgeVersion::Gradle; + out.append(fVersion); + lookup[fVersion->m_buildnr] = fVersion; + } + QJsonObject promos = root.value("promos").toObject(); + for (auto it = promos.begin(); it != promos.end(); ++it) + { + QString key = it.key(); + int build = it.value().toInt(); + QRegularExpression regexp("^(?<mcversion>[0-9]+(.[0-9]+)*)-(?<label>[a-z]+)$"); + auto match = regexp.match(key); + if(!match.hasMatch()) + { + qDebug() << key << "doesn't match." << "build" << build; + continue; + } + + QString label = match.captured("label"); + if(label != "recommended") + { + continue; + } + QString mcversion = match.captured("mcversion"); + qDebug() << "Forge build" << build << "is the" << label << "for Minecraft" << mcversion << QString("<%1>").arg(key); + lookup[build]->is_recommended = true; + } + return true; +} + +void ForgeListLoadTask::listDownloaded() +{ + QList<BaseVersionPtr> list; + bool ret = true; + if (!parseForgeList(list)) + { + ret = false; + } + if (!parseForgeGradleList(list)) + { + ret = false; + } + + if (!ret) + { + return; + } + std::sort(list.begin(), list.end(), [](const BaseVersionPtr & l, const BaseVersionPtr & r) + { return (*l > *r); }); + + m_list->updateListData(list); + + emitSucceeded(); + return; +} + +void ForgeListLoadTask::listFailed() +{ + auto &reply = listDownload->m_reply; + if (reply) + { + qCritical() << "Getting forge version list failed: " << reply->errorString(); + } + else + { + qCritical() << "Getting forge version list failed for reasons unknown."; + } +} + +void ForgeListLoadTask::gradleListFailed() +{ + auto &reply = gradleListDownload->m_reply; + if (reply) + { + qCritical() << "Getting forge version list failed: " << reply->errorString(); + } + else + { + qCritical() << "Getting forge version list failed for reasons unknown."; + } +} diff --git a/logic/minecraft/forge/ForgeVersionList.h b/logic/minecraft/forge/ForgeVersionList.h new file mode 100644 index 00000000..308503e3 --- /dev/null +++ b/logic/minecraft/forge/ForgeVersionList.h @@ -0,0 +1,90 @@ +/* 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. + */ + +#pragma once + +#include "ForgeVersion.h" + +#include <QObject> +#include <QAbstractListModel> +#include <QUrl> +#include <QNetworkReply> + +#include "BaseVersionList.h" +#include "tasks/Task.h" +#include "net/NetJob.h" + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT ForgeVersionList : public BaseVersionList +{ + Q_OBJECT +public: + friend class ForgeListLoadTask; + + explicit ForgeVersionList(QObject *parent = 0); + + virtual Task *getLoadTask() override; + virtual bool isLoaded() override; + virtual const BaseVersionPtr at(int i) const override; + virtual int count() const override; + virtual void sortVersions() override; + + virtual BaseVersionPtr getLatestStable() const override; + + ForgeVersionPtr findVersionByVersionNr(QString version); + + virtual QVariant data(const QModelIndex &index, int role) const override; + virtual QList<ModelRoles> providesRoles() override; + + virtual int columnCount(const QModelIndex &parent) const override; + +protected: + QList<BaseVersionPtr> m_vlist; + + bool m_loaded = false; + +protected +slots: + virtual void updateListData(QList<BaseVersionPtr> versions) override; +}; + +class ForgeListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit ForgeListLoadTask(ForgeVersionList *vlist); + + virtual void executeTask(); + virtual bool abort(); + +protected +slots: + void listDownloaded(); + void listFailed(); + void gradleListFailed(); + +protected: + NetJobPtr listJob; + ForgeVersionList *m_list; + + CacheDownloadPtr listDownload; + CacheDownloadPtr gradleListDownload; + +private: + bool parseForgeList(QList<BaseVersionPtr> &out); + bool parseForgeGradleList(QList<BaseVersionPtr> &out); +}; diff --git a/logic/minecraft/forge/ForgeXzDownload.cpp b/logic/minecraft/forge/ForgeXzDownload.cpp new file mode 100644 index 00000000..6009e31e --- /dev/null +++ b/logic/minecraft/forge/ForgeXzDownload.cpp @@ -0,0 +1,389 @@ +/* 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 "Env.h" +#include "ForgeXzDownload.h" +#include <FileSystem.h> + +#include <QCryptographicHash> +#include <QFileInfo> +#include <QDateTime> +#include <QDir> +#include <QDebug> + +ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction() +{ + m_entry = entry; + m_target_path = entry->getFullPath(); + m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX"); + m_status = Job_NotStarted; + m_url_path = relative_path; +} + +void ForgeXzDownload::setMirrors(QList<ForgeMirror> &mirrors) +{ + m_mirror_index = 0; + m_mirrors = mirrors; + updateUrl(); +} + +void ForgeXzDownload::start() +{ + m_status = Job_InProgress; + if (!m_entry->stale) + { + m_status = Job_Finished; + emit succeeded(m_index_within_job); + return; + } + // can we actually create the real, final file? + if (!FS::ensureFilePathExists(m_target_path)) + { + m_status = Job_Failed; + emit failed(m_index_within_job); + return; + } + if (m_mirrors.empty()) + { + m_status = Job_Failed; + emit failed(m_index_within_job); + return; + } + + qDebug() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1()); + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); + + auto worker = ENV.qnam(); + QNetworkReply *rep = worker->get(request); + + m_reply.reset(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); +} + +void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} + +void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + // TODO: log the reason why + m_status = Job_Failed; +} + +void ForgeXzDownload::failAndTryNextMirror() +{ + m_status = Job_Failed; + int next = m_mirror_index + 1; + if(m_mirrors.size() == next) + m_mirror_index = 0; + else + m_mirror_index = next; + + updateUrl(); + emit failed(m_index_within_job); +} + +void ForgeXzDownload::updateUrl() +{ + qDebug() << "Updating URL for " << m_url_path; + for (auto possible : m_mirrors) + { + qDebug() << "Possible: " << possible.name << " : " << possible.mirror_url; + } + QString aggregate = m_mirrors[m_mirror_index].mirror_url + m_url_path + ".pack.xz"; + m_url = QUrl(aggregate); +} + +void ForgeXzDownload::downloadFinished() +{ + //TEST: defer to other possible mirrors (autofail the first one) + /* + qDebug() <<"dl " << index_within_job << " mirror " << m_mirror_index; + if( m_mirror_index == 0) + { + qDebug() <<"dl " << index_within_job << " AUTOFAIL"; + m_status = Job_Failed; + m_pack200_xz_file.close(); + m_pack200_xz_file.remove(); + m_reply.reset(); + failAndTryNextMirror(); + return; + } + */ + + // if the download succeeded + if (m_status != Job_Failed) + { + // nothing went wrong... + m_status = Job_Finished; + if (m_pack200_xz_file.isOpen()) + { + // we actually downloaded something! process and isntall it + decompressAndInstall(); + return; + } + else + { + // something bad happened -- on the local machine! + m_status = Job_Failed; + m_pack200_xz_file.remove(); + m_reply.reset(); + emit failed(m_index_within_job); + return; + } + } + // else the download failed + else + { + m_status = Job_Failed; + m_pack200_xz_file.close(); + m_pack200_xz_file.remove(); + m_reply.reset(); + failAndTryNextMirror(); + return; + } +} + +void ForgeXzDownload::downloadReadyRead() +{ + + if (!m_pack200_xz_file.isOpen()) + { + if (!m_pack200_xz_file.open()) + { + /* + * Can't open the file... the job failed + */ + m_reply->abort(); + emit failed(m_index_within_job); + return; + } + } + m_pack200_xz_file.write(m_reply->readAll()); +} + +#include "xz.h" +#include "unpack200.h" +#include <stdexcept> + +const size_t buffer_size = 8196; + +void ForgeXzDownload::decompressAndInstall() +{ + // rewind the downloaded temp file + m_pack200_xz_file.seek(0); + // de-xz'd file + QTemporaryFile pack200_file("./dl_temp.XXXXXX"); + pack200_file.open(); + + bool xz_success = false; + // first, de-xz + { + uint8_t in[buffer_size]; + uint8_t out[buffer_size]; + struct xz_buf b; + struct xz_dec *s; + enum xz_ret ret; + xz_crc32_init(); + xz_crc64_init(); + s = xz_dec_init(XZ_DYNALLOC, 1 << 26); + if (s == nullptr) + { + xz_dec_end(s); + failAndTryNextMirror(); + return; + } + b.in = in; + b.in_pos = 0; + b.in_size = 0; + b.out = out; + b.out_pos = 0; + b.out_size = buffer_size; + while (!xz_success) + { + if (b.in_pos == b.in_size) + { + b.in_size = m_pack200_xz_file.read((char *)in, sizeof(in)); + b.in_pos = 0; + } + + ret = xz_dec_run(s, &b); + + if (b.out_pos == sizeof(out)) + { + if (pack200_file.write((char *)out, b.out_pos) != b.out_pos) + { + // msg = "Write error\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + } + + b.out_pos = 0; + } + + if (ret == XZ_OK) + continue; + + if (ret == XZ_UNSUPPORTED_CHECK) + { + // unsupported check. this is OK, but we should log this + continue; + } + + if (pack200_file.write((char *)out, b.out_pos) != b.out_pos) + { + // write error + pack200_file.close(); + xz_dec_end(s); + return; + } + + switch (ret) + { + case XZ_STREAM_END: + xz_dec_end(s); + xz_success = true; + break; + + case XZ_MEM_ERROR: + qCritical() << "Memory allocation failed\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + + case XZ_MEMLIMIT_ERROR: + qCritical() << "Memory usage limit reached\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + + case XZ_FORMAT_ERROR: + qCritical() << "Not a .xz file\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + + case XZ_OPTIONS_ERROR: + qCritical() << "Unsupported options in the .xz headers\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + + case XZ_DATA_ERROR: + case XZ_BUF_ERROR: + qCritical() << "File is corrupt\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + + default: + qCritical() << "Bug!\n"; + xz_dec_end(s); + failAndTryNextMirror(); + return; + } + } + } + m_pack200_xz_file.remove(); + + // revert pack200 + pack200_file.seek(0); + int handle_in = pack200_file.handle(); + // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects. + if(handle_in == -1) + { + qCritical() << "Error reopening " << pack200_file.fileName(); + failAndTryNextMirror(); + return; + } + FILE * file_in = fdopen(handle_in,"r"); + if(!file_in) + { + qCritical() << "Error reopening " << pack200_file.fileName(); + failAndTryNextMirror(); + return; + } + QFile qfile_out(m_target_path); + if(!qfile_out.open(QIODevice::WriteOnly)) + { + qCritical() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + int handle_out = qfile_out.handle(); + if(handle_out == -1) + { + qCritical() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + FILE * file_out = fdopen(handle_out,"w"); + if(!file_out) + { + qCritical() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + try + { + unpack_200(file_in, file_out); + } + catch (std::runtime_error &err) + { + m_status = Job_Failed; + qCritical() << "Error unpacking " << pack200_file.fileName() << " : " << err.what(); + QFile f(m_target_path); + if (f.exists()) + f.remove(); + failAndTryNextMirror(); + return; + } + pack200_file.remove(); + + QFile jar_file(m_target_path); + + if (!jar_file.open(QIODevice::ReadOnly)) + { + jar_file.remove(); + failAndTryNextMirror(); + return; + } + m_entry->md5sum = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5) + .toHex() + .constData(); + jar_file.close(); + + QFileInfo output_file_info(m_target_path); + m_entry->etag = m_reply->rawHeader("ETag").constData(); + m_entry->local_changed_timestamp = + output_file_info.lastModified().toUTC().toMSecsSinceEpoch(); + m_entry->stale = false; + ENV.metacache()->updateEntry(m_entry); + + m_reply.reset(); + emit succeeded(m_index_within_job); +} diff --git a/logic/minecraft/forge/ForgeXzDownload.h b/logic/minecraft/forge/ForgeXzDownload.h new file mode 100644 index 00000000..45722812 --- /dev/null +++ b/logic/minecraft/forge/ForgeXzDownload.h @@ -0,0 +1,66 @@ +/* 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. + */ + +#pragma once + +#include "net/NetAction.h" +#include "net/HttpMetaCache.h" +#include <QFile> +#include <QTemporaryFile> +#include "ForgeMirror.h" + +typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr; + +class ForgeXzDownload : public NetAction +{ + Q_OBJECT +public: + MetaEntryPtr m_entry; + /// if saving to file, use the one specified in this string + QString m_target_path; + /// this is the output file, if any + QTemporaryFile m_pack200_xz_file; + /// mirror index (NOT OPTICS, I SWEAR) + int m_mirror_index = 0; + /// list of mirrors to use. Mirror has the url base + QList<ForgeMirror> m_mirrors; + /// path relative to the mirror base + QString m_url_path; + +public: + explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry); + static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry) + { + return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry)); + } + virtual ~ForgeXzDownload(){}; + void setMirrors(QList<ForgeMirror> & mirrors); + +protected +slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead(); + +public +slots: + virtual void start(); + +private: + void decompressAndInstall(); + void failAndTryNextMirror(); + void updateUrl(); +}; diff --git a/logic/minecraft/forge/LegacyForge.cpp b/logic/minecraft/forge/LegacyForge.cpp new file mode 100644 index 00000000..aa2c8063 --- /dev/null +++ b/logic/minecraft/forge/LegacyForge.cpp @@ -0,0 +1,56 @@ +/* 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 "LegacyForge.h" + +MinecraftForge::MinecraftForge(const QString &file) : Mod(file) +{ +} + +bool MinecraftForge::FixVersionIfNeeded(QString newVersion) +{/* + wxString reportedVersion = GetModVersion(); + if(reportedVersion == "..." || reportedVersion.empty()) + { + std::auto_ptr<wxFFileInputStream> in(new wxFFileInputStream("forge.zip")); + wxTempFileOutputStream out("forge.zip"); + wxTextOutputStream textout(out); + wxZipInputStream inzip(*in); + wxZipOutputStream outzip(out); + std::auto_ptr<wxZipEntry> entry; + // preserve metadata + outzip.CopyArchiveMetaData(inzip); + // copy all entries + while (entry.reset(inzip.GetNextEntry()), entry.get() != NULL) + if (!outzip.CopyEntry(entry.release(), inzip)) + return false; + // release last entry + in.reset(); + outzip.PutNextEntry("forgeversion.properties"); + + wxStringTokenizer tokenizer(newVersion,"."); + wxString verFile; + verFile << wxString("forge.major.number=") << tokenizer.GetNextToken() << "\n"; + verFile << wxString("forge.minor.number=") << tokenizer.GetNextToken() << "\n"; + verFile << wxString("forge.revision.number=") << tokenizer.GetNextToken() << "\n"; + verFile << wxString("forge.build.number=") << tokenizer.GetNextToken() << "\n"; + auto buf = verFile.ToUTF8(); + outzip.Write(buf.data(), buf.length()); + // check if we succeeded + return inzip.Eof() && outzip.Close() && out.Commit(); + } + */ + return true; +} diff --git a/logic/minecraft/forge/LegacyForge.h b/logic/minecraft/forge/LegacyForge.h new file mode 100644 index 00000000..f51d5e85 --- /dev/null +++ b/logic/minecraft/forge/LegacyForge.h @@ -0,0 +1,25 @@ +/* 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. + */ + +#pragma once + +#include "minecraft/Mod.h" + +class MinecraftForge : public Mod +{ +public: + MinecraftForge(const QString &file); + bool FixVersionIfNeeded(QString newVersion); +}; |