diff options
author | Petr Mrázek <peterix@gmail.com> | 2016-04-10 15:53:05 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2016-05-01 00:00:14 +0200 |
commit | b6d455a02bd338e9dc0faa09d4d8177ecd8d569a (patch) | |
tree | 41982bca1ede50049f2f8c7109dd18edeefde6d0 /api/logic/minecraft | |
parent | 47e37635f50c09b4f9a9ee7699e3120bab3e4088 (diff) | |
download | MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar.gz MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar.lz MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar.xz MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.zip |
NOISSUE reorganize and document libraries
Diffstat (limited to 'api/logic/minecraft')
91 files changed, 15240 insertions, 0 deletions
diff --git a/api/logic/minecraft/AssetsUtils.cpp b/api/logic/minecraft/AssetsUtils.cpp new file mode 100644 index 00000000..7a525abe --- /dev/null +++ b/api/logic/minecraft/AssetsUtils.cpp @@ -0,0 +1,230 @@ +/* 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 <QFileInfo> +#include <QDir> +#include <QDirIterator> +#include <QCryptographicHash> +#include <QJsonParseError> +#include <QJsonDocument> +#include <QJsonObject> +#include <QVariant> +#include <QDebug> + +#include "AssetsUtils.h" +#include "FileSystem.h" +#include "net/MD5EtagDownload.h" + +namespace AssetsUtils +{ + +/* + * Returns true on success, with index populated + * index is undefined otherwise + */ +bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) +{ + /* + { + "objects": { + "icons/icon_16x16.png": { + "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", + "size": 3665 + }, + ... + } + } + } + */ + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to read assets index file" << path; + return false; + } + index->id = assetsId; + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << "Failed to parse assets index file:" << parseError.errorString() + << "at offset " << QString::number(parseError.offset); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid assets index JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + QJsonValue isVirtual = root.value("virtual"); + if (!isVirtual.isUndefined()) + { + index->isVirtual = isVirtual.toBool(false); + } + + QJsonValue objects = root.value("objects"); + QVariantMap map = objects.toVariant().toMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + // qDebug() << iter.key(); + + QVariant variant = iter.value(); + QVariantMap nested_objects = variant.toMap(); + + AssetObject object; + + for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); + nested_iter != nested_objects.end(); ++nested_iter) + { + // qDebug() << nested_iter.key() << nested_iter.value().toString(); + QString key = nested_iter.key(); + QVariant value = nested_iter.value(); + + if (key == "hash") + { + object.hash = value.toString(); + } + else if (key == "size") + { + object.size = value.toDouble(); + } + } + + index->objects.insert(iter.key(), object); + } + + return true; +} + +QDir reconstructAssets(QString assetsId) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets"; + return virtualRoot; + } + + qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() + << objectDir.path() << virtualDir.path() << virtualRoot.path(); + + AssetsIndex index; + bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, &index); + + if (loadAssetsIndex && index.isVirtual) + { + qDebug() << "Reconstructing virtual assets folder at" << virtualRoot.path(); + + for (QString map : index.objects.keys()) + { + AssetObject asset_object = index.objects.value(map); + QString target_path = FS::PathCombine(virtualRoot.path(), map); + QFile target(target_path); + + QString tlk = asset_object.hash.left(2); + + QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash); + QFile original(original_path); + if (!original.exists()) + continue; + if (!target.exists()) + { + QFileInfo info(target_path); + QDir target_dir = info.dir(); + // qDebug() << target_dir; + if (!target_dir.exists()) + QDir("").mkpath(target_dir.path()); + + bool couldCopy = original.copy(target_path); + qDebug() << " Copying" << original_path << "to" << target_path + << QString::number(couldCopy); // << original.errorString(); + } + } + + // TODO: Write last used time to virtualRoot/.lastused + } + + return virtualRoot; +} + +} + +NetActionPtr AssetObject::getDownloadAction() +{ + QFileInfo objectFile(getLocalPath()); + if ((!objectFile.isFile()) || (objectFile.size() != size)) + { + auto objectDL = MD5EtagDownload::make(getUrl(), objectFile.filePath()); + objectDL->m_total_progress = size; + return objectDL; + } + return nullptr; +} + +QString AssetObject::getLocalPath() +{ + return "assets/objects/" + getRelPath(); +} + +QUrl AssetObject::getUrl() +{ + return QUrl("http://resources.download.minecraft.net/" + getRelPath()); +} + +QString AssetObject::getRelPath() +{ + return hash.left(2) + "/" + hash; +} + +NetJobPtr AssetsIndex::getDownloadJob() +{ + auto job = new NetJob(QObject::tr("Assets for %1").arg(id)); + for (auto &object : objects.values()) + { + auto dl = object.getDownloadAction(); + if(dl) + { + job->addNetAction(dl); + } + } + if(job->size()) + return job; + return nullptr; +} diff --git a/api/logic/minecraft/AssetsUtils.h b/api/logic/minecraft/AssetsUtils.h new file mode 100644 index 00000000..90251c2d --- /dev/null +++ b/api/logic/minecraft/AssetsUtils.h @@ -0,0 +1,48 @@ +/* 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 <QString> +#include <QMap> +#include "net/NetAction.h" +#include "net/NetJob.h" + +struct AssetObject +{ + QString getRelPath(); + QUrl getUrl(); + QString getLocalPath(); + NetActionPtr getDownloadAction(); + + QString hash; + qint64 size; +}; + +struct AssetsIndex +{ + NetJobPtr getDownloadJob(); + + QString id; + QMap<QString, AssetObject> objects; + bool isVirtual = false; +}; + +namespace AssetsUtils +{ +bool loadAssetsIndexJson(QString id, QString file, AssetsIndex* index); +/// Reconstruct a virtual assets folder for the given assets ID and return the folder +QDir reconstructAssets(QString assetsId); +} diff --git a/api/logic/minecraft/GradleSpecifier.h b/api/logic/minecraft/GradleSpecifier.h new file mode 100644 index 00000000..18308537 --- /dev/null +++ b/api/logic/minecraft/GradleSpecifier.h @@ -0,0 +1,129 @@ +#pragma once + +#include <QString> +#include <QStringList> +#include "DefaultVariable.h" + +struct GradleSpecifier +{ + GradleSpecifier() + { + m_valid = false; + } + GradleSpecifier(QString value) + { + operator=(value); + } + GradleSpecifier & operator =(const QString & value) + { + /* + org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar + DEBUG 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + DEBUG 1 "org.gradle.test.classifiers" + DEBUG 2 "service" + DEBUG 3 "1.0" + DEBUG 4 ":jdk15" + DEBUG 5 "jdk15" + DEBUG 6 "@jar" + DEBUG 7 "jar" + */ + QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(:([^:@]+))?" "(@([^:@]+))?"); + m_valid = matcher.exactMatch(value); + auto elements = matcher.capturedTexts(); + m_groupId = elements[1]; + m_artifactId = elements[2]; + m_version = elements[3]; + m_classifier = elements[5]; + if(!elements[7].isEmpty()) + { + m_extension = elements[7]; + } + return *this; + } + operator QString() const + { + if(!m_valid) + return "INVALID"; + QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; + if(!m_classifier.isEmpty()) + { + retval += ":" + m_classifier; + } + if(m_extension.isExplicit()) + { + retval += "@" + m_extension; + } + return retval; + } + QString toPath() const + { + if(!m_valid) + return "INVALID"; + QString path = m_groupId; + path.replace('.', '/'); + path += '/' + m_artifactId + '/' + m_version + '/' + m_artifactId + '-' + m_version; + if(!m_classifier.isEmpty()) + { + path += "-" + m_classifier; + } + path += "." + m_extension; + return path; + } + inline bool valid() const + { + return m_valid; + } + inline QString version() const + { + return m_version; + } + inline QString groupId() const + { + return m_groupId; + } + inline QString artifactId() const + { + return m_artifactId; + } + inline void setClassifier(const QString & classifier) + { + m_classifier = classifier; + } + inline QString classifier() const + { + return m_classifier; + } + inline QString extension() const + { + return m_extension; + } + inline QString artifactPrefix() const + { + return m_groupId + ":" + m_artifactId; + } + bool matchName(const GradleSpecifier & other) const + { + return other.artifactId() == artifactId() && other.groupId() == groupId(); + } + bool operator==(const GradleSpecifier & other) const + { + if(m_groupId != other.m_groupId) + return false; + if(m_artifactId != other.m_artifactId) + return false; + if(m_version != other.m_version) + return false; + if(m_classifier != other.m_classifier) + return false; + if(m_extension != other.m_extension) + return false; + return true; + } +private: + QString m_groupId; + QString m_artifactId; + QString m_version; + QString m_classifier; + DefaultVariable<QString> m_extension = DefaultVariable<QString>("jar"); + bool m_valid = false; +}; diff --git a/api/logic/minecraft/JarMod.h b/api/logic/minecraft/JarMod.h new file mode 100644 index 00000000..42d05da9 --- /dev/null +++ b/api/logic/minecraft/JarMod.h @@ -0,0 +1,12 @@ +#pragma once +#include <QString> +#include <QJsonObject> +#include <memory> +class Jarmod; +typedef std::shared_ptr<Jarmod> JarmodPtr; +class Jarmod +{ +public: /* data */ + QString name; + QString originalName; +}; diff --git a/api/logic/minecraft/Library.cpp b/api/logic/minecraft/Library.cpp new file mode 100644 index 00000000..922db84e --- /dev/null +++ b/api/logic/minecraft/Library.cpp @@ -0,0 +1,239 @@ +#include "Library.h" +#include <net/CacheDownload.h> +#include <minecraft/forge/ForgeXzDownload.h> +#include <Env.h> +#include <FileSystem.h> + +void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64) const +{ + auto actualPath = [&](QString relPath) + { + QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); + return out.absoluteFilePath(); + }; + if(m_mojangDownloads) + { + if(m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + jar += actualPath(artifact->path); + } + if(!isNative()) + return; + if(m_nativeClassifiers.contains(system)) + { + auto nativeClassifier = m_nativeClassifiers[system]; + if(nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if(nat32info) + native32 += actualPath(nat32info->path); + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if(nat64info) + native64 += actualPath(nat64info->path); + } + else + { + native += actualPath(m_mojangDownloads->getDownloadInfo(nativeClassifier)->path); + } + } + } + else + { + QString raw_storage = storageSuffix(system); + if(isNative()) + { + if (raw_storage.contains("${arch}")) + { + auto nat32Storage = raw_storage; + nat32Storage.replace("${arch}", "32"); + auto nat64Storage = raw_storage; + nat64Storage.replace("${arch}", "64"); + native32 += actualPath(nat32Storage); + native64 += actualPath(nat64Storage); + } + else + { + native += actualPath(raw_storage); + } + } + else + { + jar += actualPath(raw_storage); + } + } +} + +QList<NetActionPtr> Library::getDownloads(OpSys system, HttpMetaCache * cache, QStringList &failedFiles) const +{ + QList<NetActionPtr> out; + bool isLocal = (hint() == "local"); + bool isForge = (hint() == "forge-pack-xz"); + + auto add_download = [&](QString storage, QString dl) + { + auto entry = cache->resolveEntry("libraries", storage); + if (!entry->isStale()) + return true; + if(isLocal) + { + QFileInfo fileinfo(entry->getFullPath()); + if(!fileinfo.exists()) + { + failedFiles.append(entry->getFullPath()); + return false; + } + return true; + } + if (isForge) + { + out.append(ForgeXzDownload::make(storage, entry)); + } + else + { + out.append(CacheDownload::make(dl, entry)); + } + return true; + }; + + if(m_mojangDownloads) + { + if(m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + add_download(artifact->path, artifact->url); + } + if(m_nativeClassifiers.contains(system)) + { + auto nativeClassifier = m_nativeClassifiers[system]; + if(nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if(nat32info) + add_download(nat32info->path, nat32info->url); + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if(nat64info) + add_download(nat64info->path, nat64info->url); + } + else + { + auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); + if(info) + { + add_download(info->path, info->url); + } + } + } + } + else + { + QString raw_storage = storageSuffix(system); + auto raw_dl = [&](){ + if (!m_absoluteURL.isEmpty()) + { + return m_absoluteURL; + } + + if (m_repositoryURL.isEmpty()) + { + return QString("https://" + URLConstants::LIBRARY_BASE) + raw_storage; + } + + if(m_repositoryURL.endsWith('/')) + { + return m_repositoryURL + raw_storage; + } + else + { + return m_repositoryURL + QChar('/') + raw_storage; + } + }(); + if (raw_storage.contains("${arch}")) + { + QString cooked_storage = raw_storage; + QString cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32")); + cooked_storage = raw_storage; + cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64")); + } + else + { + add_download(raw_storage, raw_dl); + } + } + return out; +} + +bool Library::isActive() const +{ + bool result = true; + if (m_rules.empty()) + { + result = true; + } + else + { + RuleAction ruleResult = Disallow; + for (auto rule : m_rules) + { + RuleAction temp = rule->apply(this); + if (temp != Defer) + ruleResult = temp; + } + result = result && (ruleResult == Allow); + } + if (isNative()) + { + result = result && m_nativeClassifiers.contains(currentSystem); + } + return result; +} + +void Library::setStoragePrefix(QString prefix) +{ + m_storagePrefix = prefix; +} + +QString Library::defaultStoragePrefix() +{ + return "libraries/"; +} + +QString Library::storagePrefix() const +{ + if(m_storagePrefix.isEmpty()) + { + return defaultStoragePrefix(); + } + return m_storagePrefix; +} + +QString Library::storageSuffix(OpSys system) const +{ + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.toPath(); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + if (m_nativeClassifiers.contains(system)) + { + nativeSpec.setClassifier(m_nativeClassifiers[system]); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.toPath(); +} diff --git a/api/logic/minecraft/Library.h b/api/logic/minecraft/Library.h new file mode 100644 index 00000000..fdce93f3 --- /dev/null +++ b/api/logic/minecraft/Library.h @@ -0,0 +1,184 @@ +#pragma once +#include <QString> +#include <net/NetAction.h> +#include <QPair> +#include <QList> +#include <QStringList> +#include <QMap> +#include <QDir> +#include <QUrl> +#include <memory> + +#include "Rule.h" +#include "minecraft/OpSys.h" +#include "GradleSpecifier.h" +#include "net/URLConstants.h" +#include "MojangDownloadInfo.h" + +#include "multimc_logic_export.h" + +class Library; + +typedef std::shared_ptr<Library> LibraryPtr; + +class MULTIMC_LOGIC_EXPORT Library +{ + friend class OneSixVersionFormat; + friend class MojangVersionFormat; + friend class LibraryTest; +public: + Library() + { + } + Library(const QString &name) + { + m_name = name; + } + /// limited copy without some data. TODO: why? + static LibraryPtr limitedCopy(LibraryPtr base) + { + auto newlib = std::make_shared<Library>(); + newlib->m_name = base->m_name; + newlib->m_repositoryURL = base->m_repositoryURL; + newlib->m_hint = base->m_hint; + newlib->m_absoluteURL = base->m_absoluteURL; + newlib->m_extractExcludes = base->m_extractExcludes; + newlib->m_nativeClassifiers = base->m_nativeClassifiers; + newlib->m_rules = base->m_rules; + newlib->m_storagePrefix = base->m_storagePrefix; + newlib->m_mojangDownloads = base->m_mojangDownloads; + return newlib; + } + +public: /* methods */ + /// Returns the raw name field + const GradleSpecifier & rawName() const + { + return m_name; + } + + void setRawName(const GradleSpecifier & spec) + { + m_name = spec; + } + + void setClassifier(const QString & spec) + { + m_name.setClassifier(spec); + } + + /// returns the full group and artifact prefix + QString artifactPrefix() const + { + return m_name.artifactPrefix(); + } + + /// get the artifact ID + QString artifactId() const + { + return m_name.artifactId(); + } + + /// get the artifact version + QString version() const + { + return m_name.version(); + } + + /// Returns true if the library is native + bool isNative() const + { + return m_nativeClassifiers.size() != 0; + } + + void setStoragePrefix(QString prefix = QString()); + + /// Set the url base for downloads + void setRepositoryURL(const QString &base_url) + { + m_repositoryURL = base_url; + } + + void getApplicableFiles(OpSys system, QStringList & jar, QStringList & native, QStringList & native32, QStringList & native64) const; + + void setAbsoluteUrl(const QString &absolute_url) + { + m_absoluteURL = absolute_url; + } + + void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) + { + m_mojangDownloads = info; + } + + void setHint(const QString &hint) + { + m_hint = hint; + } + + /// Set the load rules + void setRules(QList<std::shared_ptr<Rule>> rules) + { + m_rules = rules; + } + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive() const; + + // Get a list of downloads for this library + QList<NetActionPtr> getDownloads(OpSys system, class HttpMetaCache * cache, QStringList &failedFiles) const; + +private: /* methods */ + /// the default storage prefix used by MultiMC + static QString defaultStoragePrefix(); + + /// Get the prefix - root of the storage to be used + QString storagePrefix() const; + + /// Get the relative path where the library should be saved + QString storageSuffix(OpSys system) const; + + QString hint() const + { + return m_hint; + } + +protected: /* data */ + /// the basic gradle dependency specifier. + GradleSpecifier m_name; + + /// DEPRECATED URL prefix of the maven repo where the file can be downloaded + QString m_repositoryURL; + + /// DEPRECATED: MultiMC-specific absolute URL. takes precedence over the implicit maven repo URL, if defined + QString m_absoluteURL; + + /** + * MultiMC-specific type hint - modifies how the library is treated + */ + QString m_hint; + + /** + * storage - by default the local libraries folder in multimc, but could be elsewhere + * MultiMC specific, because of FTB. + */ + QString m_storagePrefix; + + /// true if the library had an extract/excludes section (even empty) + bool m_hasExcludes = false; + + /// a list of files that shouldn't be extracted from the library + QStringList m_extractExcludes; + + /// native suffixes per OS + QMap<OpSys, QString> m_nativeClassifiers; + + /// true if the library had a rules section (even empty) + bool applyRules = false; + + /// rules associated with the library + QList<std::shared_ptr<Rule>> m_rules; + + /// MOJANG: container with Mojang style download info + MojangLibraryDownloadInfo::Ptr m_mojangDownloads; +}; diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp new file mode 100644 index 00000000..405ccd26 --- /dev/null +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -0,0 +1,369 @@ +#include "MinecraftInstance.h" +#include <settings/Setting.h> +#include "settings/SettingsObject.h" +#include "Env.h" +#include "minecraft/MinecraftVersionList.h" +#include <MMCStrings.h> +#include <pathmatcher/RegexpMatcher.h> +#include <pathmatcher/MultiMatcher.h> +#include <FileSystem.h> +#include <java/JavaVersion.h> + +#define IBUS "@im=ibus" + +// all of this because keeping things compatible with deprecated old settings +// if either of the settings {a, b} is true, this also resolves to true +class OrSetting : public Setting +{ + Q_OBJECT +public: + OrSetting(QString id, std::shared_ptr<Setting> a, std::shared_ptr<Setting> b) + :Setting({id}, false), m_a(a), m_b(b) + { + } + virtual QVariant get() const + { + bool a = m_a->get().toBool(); + bool b = m_b->get().toBool(); + return a || b; + } + virtual void reset() {} + virtual void set(QVariant value) {} +private: + std::shared_ptr<Setting> m_a; + std::shared_ptr<Setting> m_b; +}; + +MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : BaseInstance(globalSettings, settings, rootDir) +{ + // Java Settings + auto javaOverride = m_settings->registerSetting("OverrideJava", false); + auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); + auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); + + // combinations + auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride); + auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride); + + m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation); + m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs); + + // special! + m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation); + m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation); + + // Window Size + auto windowSetting = m_settings->registerSetting("OverrideWindow", false); + m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting); + m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting); + m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting); + + // Memory + auto memorySetting = m_settings->registerSetting("OverrideMemory", false); + m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting); + m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting); + m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting); +} + +QString MinecraftInstance::minecraftRoot() const +{ + QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); + + if (dotMCDir.exists() && !mcDir.exists()) + return dotMCDir.filePath(); + else + return mcDir.filePath(); +} + +std::shared_ptr< BaseVersionList > MinecraftInstance::versionList() const +{ + return ENV.getVersionList("net.minecraft"); +} + +QStringList MinecraftInstance::javaArguments() const +{ + QStringList args; + + // custom args go first. we want to override them if we have our own here. + args.append(extraArguments()); + + // OSX dock icon and name +#ifdef Q_OS_MAC + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); +#endif + + // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 +#ifdef Q_OS_WIN32 + args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" + "minecraft.exe.heapdump"); +#endif + + args << QString("-Xms%1m").arg(settings()->get("MinMemAlloc").toInt()); + args << QString("-Xmx%1m").arg(settings()->get("MaxMemAlloc").toInt()); + + // No PermGen in newer java. + JavaVersion javaVersion(settings()->get("JavaVersion").toString()); + if(javaVersion.requiresPermGen()) + { + auto permgen = settings()->get("PermGen").toInt(); + if (permgen != 64) + { + args << QString("-XX:PermSize=%1m").arg(permgen); + } + } + + args << "-Duser.language=en"; + args << "-jar" << FS::PathCombine(QCoreApplication::applicationDirPath(), "jars", "NewLaunch.jar"); + + return args; +} + +QMap<QString, QString> MinecraftInstance::getVariables() const +{ + QMap<QString, QString> out; + out.insert("INST_NAME", name()); + out.insert("INST_ID", id()); + out.insert("INST_DIR", QDir(instanceRoot()).absolutePath()); + out.insert("INST_MC_DIR", QDir(minecraftRoot()).absolutePath()); + out.insert("INST_JAVA", settings()->get("JavaPath").toString()); + out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); + return out; +} + +static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH) +{ + QDir mmcBin(QCoreApplication::applicationDirPath()); + auto items = LD_LIBRARY_PATH.split(':'); + QStringList final; + for(auto & item: items) + { + QDir test(item); + if(test == mmcBin) + { + qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item; + continue; + } + final.append(item); + } + return final.join(':'); +} + +QProcessEnvironment MinecraftInstance::createEnvironment() +{ + // prepare the process environment + QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); + QProcessEnvironment env; + + QStringList ignored = + { + "JAVA_ARGS", + "CLASSPATH", + "CONFIGPATH", + "JAVA_HOME", + "JRE_HOME", + "_JAVA_OPTIONS", + "JAVA_OPTIONS", + "JAVA_TOOL_OPTIONS" + }; + for(auto key: rawenv.keys()) + { + auto value = rawenv.value(key); + // filter out dangerous java crap + if(ignored.contains(key)) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // filter MultiMC-related things + if(key.startsWith("QT_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } +#ifdef Q_OS_LINUX + // Do not pass LD_* variables to java. They were intended for MultiMC + if(key.startsWith("LD_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // Strip IBus + // IBus is a Linux IME framework. For some reason, it breaks MC? + if (key == "XMODIFIERS" && value.contains(IBUS)) + { + QString save = value; + value.replace(IBUS, ""); + qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; + } + if(key == "GAME_PRELOAD") + { + env.insert("LD_PRELOAD", value); + continue; + } + if(key == "GAME_LIBRARY_PATH") + { + env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value)); + continue; + } +#endif + qDebug() << "Env: " << key << value; + env.insert(key, value); + } +#ifdef Q_OS_LINUX + // HACK: Workaround for QTBUG42500 + if(!env.contains("LD_LIBRARY_PATH")) + { + env.insert("LD_LIBRARY_PATH", ""); + } +#endif + + // export some infos + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + env.insert(it.key(), it.value()); + } + return env; +} + +QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) +{ + if(!session) + { + return QMap<QString, QString>(); + } + auto & sessionRef = *session.get(); + QMap<QString, QString> filter; + auto addToFilter = [&filter](QString key, QString value) + { + if(key.trimmed().size()) + { + filter[key] = value; + } + }; + if (sessionRef.session != "-") + { + addToFilter(sessionRef.session, tr("<SESSION ID>")); + } + addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>")); + addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>")); + addToFilter(sessionRef.uuid, tr("<PROFILE ID>")); + addToFilter(sessionRef.player_name, tr("<PROFILE NAME>")); + + auto i = sessionRef.u.properties.begin(); + while (i != sessionRef.u.properties.end()) + { + addToFilter(i.value(), "<" + i.key().toUpper() + ">"); + ++i; + } + return filter; +} + +MessageLevel::Enum MinecraftInstance::guessLevel(const QString &line, MessageLevel::Enum level) +{ + QRegularExpression re("\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]"); + auto match = re.match(line); + if(match.hasMatch()) + { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + if(levelStr == "INFO") + level = MessageLevel::Message; + if(levelStr == "WARN") + level = MessageLevel::Warning; + if(levelStr == "ERROR") + level = MessageLevel::Error; + if(levelStr == "FATAL") + level = MessageLevel::Fatal; + if(levelStr == "TRACE" || levelStr == "DEBUG") + level = MessageLevel::Debug; + } + else + { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || + line.contains("[FINER]") || line.contains("[FINEST]")) + level = MessageLevel::Message; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + if (line.contains("[WARNING]")) + level = MessageLevel::Warning; + if (line.contains("[DEBUG]")) + level = MessageLevel::Debug; + } + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + //NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * + static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"; + if (line.contains("Exception in thread") + || line.contains(QRegularExpression("\\s+at " + javaSymbol)) + || line.contains(QRegularExpression("Caused by: " + javaSymbol)) + || line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) + || line.contains(QRegularExpression("... \\d+ more$")) + ) + return MessageLevel::Error; + return level; +} + +IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() +{ + auto combined = std::make_shared<MultiMatcher>(); + combined->add(std::make_shared<RegexpMatcher>(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); + combined->add(std::make_shared<RegexpMatcher>("crash-.*\\.txt")); + combined->add(std::make_shared<RegexpMatcher>("IDMap dump.*\\.txt$")); + combined->add(std::make_shared<RegexpMatcher>("ModLoader\\.txt(\\..*)?$")); + return combined; +} + +QString MinecraftInstance::getLogFileRoot() +{ + return minecraftRoot(); +} + +QString MinecraftInstance::prettifyTimeDuration(int64_t duration) +{ + int seconds = (int) (duration % 60); + duration /= 60; + int minutes = (int) (duration % 60); + duration /= 60; + int hours = (int) (duration % 24); + int days = (int) (duration / 24); + if((hours == 0)&&(days == 0)) + { + return tr("%1m %2s").arg(minutes).arg(seconds); + } + if (days == 0) + { + return tr("%1h %2m").arg(hours).arg(minutes); + } + return tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes); +} + +QString MinecraftInstance::getStatusbarDescription() +{ + QStringList traits; + if (flags() & VersionBrokenFlag) + { + traits.append(tr("broken")); + } + + QString description; + description.append(tr("Minecraft %1 (%2)").arg(intendedVersionId()).arg(typeName())); + if(totalTimePlayed() > 0) + { + description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + } + /* + if(traits.size()) + { + description.append(QString(" (%1)").arg(traits.join(", "))); + } + */ + return description; +} + +#include "MinecraftInstance.moc" diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h new file mode 100644 index 00000000..cd3a8d90 --- /dev/null +++ b/api/logic/minecraft/MinecraftInstance.h @@ -0,0 +1,69 @@ +#pragma once +#include "BaseInstance.h" +#include "minecraft/Mod.h" +#include <QProcess> + +#include "multimc_logic_export.h" + +class ModList; +class WorldList; + +class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance +{ +public: + MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual ~MinecraftInstance() {}; + + /// Path to the instance's minecraft directory. + QString minecraftRoot() const; + + ////// Mod Lists ////// + virtual std::shared_ptr<ModList> resourcePackList() const + { + return nullptr; + } + virtual std::shared_ptr<ModList> texturePackList() const + { + return nullptr; + } + virtual std::shared_ptr<WorldList> worldList() const + { + return nullptr; + } + /// get all jar mods applicable to this instance's jar + virtual QList<Mod> getJarMods() const + { + return QList<Mod>(); + } + + /// get the launch script to be used with this + virtual QString createLaunchScript(AuthSessionPtr session) = 0; + + //FIXME: nuke? + virtual std::shared_ptr<BaseVersionList> versionList() const override; + + /// get arguments passed to java + QStringList javaArguments() const; + + /// get variables for launch command variable substitution/environment + virtual QMap<QString, QString> getVariables() const override; + + /// create an environment for launching processes + virtual QProcessEnvironment createEnvironment() override; + + /// guess log level from a line of minecraft log + virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) override; + + virtual IPathMatcher::Ptr getLogFileMatcher() override; + + virtual QString getLogFileRoot() override; + + virtual QString getStatusbarDescription() override; + +protected: + QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session); +private: + QString prettifyTimeDuration(int64_t duration); +}; + +typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr; diff --git a/api/logic/minecraft/MinecraftProfile.cpp b/api/logic/minecraft/MinecraftProfile.cpp new file mode 100644 index 00000000..70d0cee4 --- /dev/null +++ b/api/logic/minecraft/MinecraftProfile.cpp @@ -0,0 +1,610 @@ +/* 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 <QFile> +#include <QCryptographicHash> +#include <Version.h> +#include <QDir> +#include <QJsonDocument> +#include <QJsonArray> +#include <QDebug> + +#include "minecraft/MinecraftProfile.h" +#include "ProfileUtils.h" +#include "ProfileStrategy.h" +#include "Exception.h" + +MinecraftProfile::MinecraftProfile(ProfileStrategy *strategy) + : QAbstractListModel() +{ + setStrategy(strategy); + clear(); +} + +void MinecraftProfile::setStrategy(ProfileStrategy* strategy) +{ + Q_ASSERT(strategy != nullptr); + + if(m_strategy != nullptr) + { + delete m_strategy; + m_strategy = nullptr; + } + m_strategy = strategy; + m_strategy->profile = this; +} + +ProfileStrategy* MinecraftProfile::strategy() +{ + return m_strategy; +} + +void MinecraftProfile::reload() +{ + beginResetModel(); + m_strategy->load(); + reapplyPatches(); + endResetModel(); +} + +void MinecraftProfile::clear() +{ + m_minecraftVersion.clear(); + m_minecraftVersionType.clear(); + m_minecraftAssets.reset(); + m_minecraftArguments.clear(); + m_tweakers.clear(); + m_mainClass.clear(); + m_appletClass.clear(); + m_libraries.clear(); + m_traits.clear(); + m_jarMods.clear(); + mojangDownloads.clear(); + m_problemSeverity = ProblemSeverity::PROBLEM_NONE; +} + +void MinecraftProfile::clearPatches() +{ + beginResetModel(); + m_patches.clear(); + endResetModel(); +} + +void MinecraftProfile::appendPatch(ProfilePatchPtr patch) +{ + int index = m_patches.size(); + beginInsertRows(QModelIndex(), index, index); + m_patches.append(patch); + endInsertRows(); +} + +bool MinecraftProfile::remove(const int index) +{ + auto patch = versionPatch(index); + if (!patch->isRemovable()) + { + qDebug() << "Patch" << patch->getID() << "is non-removable"; + return false; + } + + if(!m_strategy->removePatch(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be removed"; + return false; + } + + beginRemoveRows(QModelIndex(), index, index); + m_patches.removeAt(index); + endRemoveRows(); + reapplyPatches(); + saveCurrentOrder(); + return true; +} + +bool MinecraftProfile::remove(const QString id) +{ + int i = 0; + for (auto patch : m_patches) + { + if (patch->getID() == id) + { + return remove(i); + } + i++; + } + return false; +} + +bool MinecraftProfile::customize(int index) +{ + auto patch = versionPatch(index); + if (!patch->isCustomizable()) + { + qDebug() << "Patch" << patch->getID() << "is not customizable"; + return false; + } + if(!m_strategy->customizePatch(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be customized"; + return false; + } + reapplyPatches(); + saveCurrentOrder(); + // FIXME: maybe later in unstable + // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + return true; +} + +bool MinecraftProfile::revertToBase(int index) +{ + auto patch = versionPatch(index); + if (!patch->isRevertible()) + { + qDebug() << "Patch" << patch->getID() << "is not revertible"; + return false; + } + if(!m_strategy->revertPatch(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be reverted"; + return false; + } + reapplyPatches(); + saveCurrentOrder(); + // FIXME: maybe later in unstable + // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + return true; +} + +ProfilePatchPtr MinecraftProfile::versionPatch(const QString &id) +{ + for (auto file : m_patches) + { + if (file->getID() == id) + { + return file; + } + } + return nullptr; +} + +ProfilePatchPtr MinecraftProfile::versionPatch(int index) +{ + if(index < 0 || index >= m_patches.size()) + return nullptr; + return m_patches[index]; +} + +bool MinecraftProfile::isVanilla() +{ + for(auto patchptr: m_patches) + { + if(patchptr->isCustom()) + return false; + } + return true; +} + +bool MinecraftProfile::revertToVanilla() +{ + // remove patches, if present + auto VersionPatchesCopy = m_patches; + for(auto & it: VersionPatchesCopy) + { + if (!it->isCustom()) + { + continue; + } + if(it->isRevertible() || it->isRemovable()) + { + if(!remove(it->getID())) + { + qWarning() << "Couldn't remove" << it->getID() << "from profile!"; + reapplyPatches(); + saveCurrentOrder(); + return false; + } + } + } + reapplyPatches(); + saveCurrentOrder(); + return true; +} + +QVariant MinecraftProfile::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= m_patches.size()) + return QVariant(); + + auto patch = m_patches.at(row); + + if (role == Qt::DisplayRole) + { + switch (column) + { + case 0: + return m_patches.at(row)->getName(); + case 1: + { + if(patch->isCustom()) + { + return QString("%1 (Custom)").arg(patch->getVersion()); + } + else + { + return patch->getVersion(); + } + } + default: + return QVariant(); + } + } + if(role == Qt::DecorationRole) + { + switch(column) + { + case 0: + { + auto severity = patch->getProblemSeverity(); + switch (severity) + { + case PROBLEM_WARNING: + return "warning"; + case PROBLEM_ERROR: + return "error"; + default: + return QVariant(); + } + } + default: + { + return QVariant(); + } + } + } + return QVariant(); +} +QVariant MinecraftProfile::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + { + if (role == Qt::DisplayRole) + { + switch (section) + { + case 0: + return tr("Name"); + case 1: + return tr("Version"); + default: + return QVariant(); + } + } + } + return QVariant(); +} +Qt::ItemFlags MinecraftProfile::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +int MinecraftProfile::rowCount(const QModelIndex &parent) const +{ + return m_patches.size(); +} + +int MinecraftProfile::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +void MinecraftProfile::saveCurrentOrder() const +{ + ProfileUtils::PatchOrder order; + for(auto item: m_patches) + { + if(!item->isMoveable()) + continue; + order.append(item->getID()); + } + m_strategy->saveOrder(order); +} + +void MinecraftProfile::move(const int index, const MoveDirection direction) +{ + int theirIndex; + if (direction == MoveUp) + { + theirIndex = index - 1; + } + else + { + theirIndex = index + 1; + } + + if (index < 0 || index >= m_patches.size()) + return; + if (theirIndex >= rowCount()) + theirIndex = rowCount() - 1; + if (theirIndex == -1) + theirIndex = rowCount() - 1; + if (index == theirIndex) + return; + int togap = theirIndex > index ? theirIndex + 1 : theirIndex; + + auto from = versionPatch(index); + auto to = versionPatch(theirIndex); + + if (!from || !to || !to->isMoveable() || !from->isMoveable()) + { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); + m_patches.swap(index, theirIndex); + endMoveRows(); + reapplyPatches(); + saveCurrentOrder(); +} +void MinecraftProfile::resetOrder() +{ + m_strategy->resetOrder(); + reload(); +} + +bool MinecraftProfile::reapplyPatches() +{ + try + { + clear(); + for(auto file: m_patches) + { + file->applyTo(this); + } + } + catch (Exception & error) + { + clear(); + qWarning() << "Couldn't apply profile patches because: " << error.cause(); + return false; + } + return true; +} + +static void applyString(const QString & from, QString & to) +{ + if(from.isEmpty()) + return; + to = from; +} + +void MinecraftProfile::applyMinecraftVersion(const QString& id) +{ + applyString(id, this->m_minecraftVersion); +} + +void MinecraftProfile::applyAppletClass(const QString& appletClass) +{ + applyString(appletClass, this->m_appletClass); +} + +void MinecraftProfile::applyMainClass(const QString& mainClass) +{ + applyString(mainClass, this->m_mainClass); +} + +void MinecraftProfile::applyMinecraftArguments(const QString& minecraftArguments) +{ + applyString(minecraftArguments, this->m_minecraftArguments); +} + +void MinecraftProfile::applyMinecraftVersionType(const QString& type) +{ + applyString(type, this->m_minecraftVersionType); +} + +void MinecraftProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) +{ + if(assets) + { + m_minecraftAssets = assets; + } +} + +void MinecraftProfile::applyMojangDownload(const QString &key, MojangDownloadInfo::Ptr download) +{ + if(download) + { + mojangDownloads[key] = download; + } + else + { + mojangDownloads.remove(key); + } +} + +void MinecraftProfile::applyTraits(const QSet<QString>& traits) +{ + this->m_traits.unite(traits); +} + +void MinecraftProfile::applyTweakers(const QStringList& tweakers) +{ + // FIXME: check for dupes? + // FIXME: does order matter? + for (auto tweaker : tweakers) + { + this->m_tweakers += tweaker; + } +} + +void MinecraftProfile::applyJarMods(const QList<JarmodPtr>& jarMods) +{ + this->m_jarMods.append(jarMods); +} + +static int findLibraryByName(QList<LibraryPtr> haystack, const GradleSpecifier &needle) +{ + int retval = -1; + for (int i = 0; i < haystack.size(); ++i) + { + if (haystack.at(i)->rawName().matchName(needle)) + { + // only one is allowed. + if (retval != -1) + return -1; + retval = i; + } + } + return retval; +} + +void MinecraftProfile::applyLibrary(LibraryPtr library) +{ + if(!library->isActive()) + { + return; + } + // find the library by name. + const int index = findLibraryByName(m_libraries, library->rawName()); + // library not found? just add it. + if (index < 0) + { + m_libraries.append(Library::limitedCopy(library)); + return; + } + auto existingLibrary = m_libraries.at(index); + // if we are higher it means we should update + if (Version(library->version()) > Version(existingLibrary->version())) + { + auto libraryCopy = Library::limitedCopy(library); + m_libraries.replace(index, libraryCopy); + } +} + +void MinecraftProfile::applyProblemSeverity(ProblemSeverity severity) +{ + if (m_problemSeverity < severity) + { + m_problemSeverity = severity; + } +} + + +QString MinecraftProfile::getMinecraftVersion() const +{ + return m_minecraftVersion; +} + +QString MinecraftProfile::getAppletClass() const +{ + return m_appletClass; +} + +QString MinecraftProfile::getMainClass() const +{ + return m_mainClass; +} + +const QSet<QString> &MinecraftProfile::getTraits() const +{ + return m_traits; +} + +const QStringList & MinecraftProfile::getTweakers() const +{ + return m_tweakers; +} + +bool MinecraftProfile::hasTrait(const QString& trait) const +{ + return m_traits.contains(trait); +} + +ProblemSeverity MinecraftProfile::getProblemSeverity() const +{ + return m_problemSeverity; +} + +QString MinecraftProfile::getMinecraftVersionType() const +{ + return m_minecraftVersionType; +} + +std::shared_ptr<MojangAssetIndexInfo> MinecraftProfile::getMinecraftAssets() const +{ + if(!m_minecraftAssets) + { + return std::make_shared<MojangAssetIndexInfo>("legacy"); + } + return m_minecraftAssets; +} + +QString MinecraftProfile::getMinecraftArguments() const +{ + return m_minecraftArguments; +} + +const QList<JarmodPtr> & MinecraftProfile::getJarMods() const +{ + return m_jarMods; +} + +const QList<LibraryPtr> & MinecraftProfile::getLibraries() const +{ + return m_libraries; +} + +QString MinecraftProfile::getMainJarUrl() const +{ + auto iter = mojangDownloads.find("client"); + if(iter != mojangDownloads.end()) + { + // current + return iter.value()->url; + } + else + { + // legacy fallback + return URLConstants::getLegacyJarUrl(getMinecraftVersion()); + } +} + +void MinecraftProfile::installJarMods(QStringList selectedFiles) +{ + m_strategy->installJarMods(selectedFiles); +} + +/* + * TODO: get rid of this. Get rid of all order numbers. + */ +int MinecraftProfile::getFreeOrderNumber() +{ + int largest = 100; + // yes, I do realize this is dumb. The order thing itself is dumb. and to be removed next. + for(auto thing: m_patches) + { + int order = thing->getOrder(); + if(order > largest) + largest = order; + } + return largest + 1; +} diff --git a/api/logic/minecraft/MinecraftProfile.h b/api/logic/minecraft/MinecraftProfile.h new file mode 100644 index 00000000..ca9288ad --- /dev/null +++ b/api/logic/minecraft/MinecraftProfile.h @@ -0,0 +1,200 @@ +/* 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 <QAbstractListModel> + +#include <QString> +#include <QList> +#include <memory> + +#include "Library.h" +#include "VersionFile.h" +#include "JarMod.h" +#include "MojangDownloadInfo.h" + +#include "multimc_logic_export.h" + +class ProfileStrategy; +class OneSixInstance; + + +class MULTIMC_LOGIC_EXPORT MinecraftProfile : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit MinecraftProfile(ProfileStrategy *strategy); + + void setStrategy(ProfileStrategy * strategy); + ProfileStrategy *strategy(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex &parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + + /// is this version unchanged by the user? + bool isVanilla(); + + /// remove any customizations on top of whatever 'vanilla' means + bool revertToVanilla(); + + /// install more jar mods + void installJarMods(QStringList selectedFiles); + + /// DEPRECATED, remove ASAP + int getFreeOrderNumber(); + + enum MoveDirection { MoveUp, MoveDown }; + /// move patch file # up or down the list + void move(const int index, const MoveDirection direction); + + /// remove patch file # - including files/records + bool remove(const int index); + + /// remove patch file by id - including files/records + bool remove(const QString id); + + bool customize(int index); + + bool revertToBase(int index); + + void resetOrder(); + + /// reload all profile patches from storage, clear the profile and apply the patches + void reload(); + + /// clear the profile + void clear(); + + /// apply the patches. Catches all the errors and returns true/false for success/failure + bool reapplyPatches(); + +public: /* application of profile variables from patches */ + void applyMinecraftVersion(const QString& id); + void applyMainClass(const QString& mainClass); + void applyAppletClass(const QString& appletClass); + void applyMinecraftArguments(const QString& minecraftArguments); + void applyMinecraftVersionType(const QString& type); + void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); + void applyTraits(const QSet<QString> &traits); + void applyTweakers(const QStringList &tweakers); + void applyJarMods(const QList<JarmodPtr> &jarMods); + void applyLibrary(LibraryPtr library); + void applyProblemSeverity(ProblemSeverity severity); + void applyMojangDownload(const QString & key, MojangDownloadInfo::Ptr download); + +public: /* getters for profile variables */ + QString getMinecraftVersion() const; + QString getMainClass() const; + QString getAppletClass() const; + QString getMinecraftVersionType() const; + MojangAssetIndexInfo::Ptr getMinecraftAssets() const; + QString getMinecraftArguments() const; + const QSet<QString> & getTraits() const; + const QStringList & getTweakers() const; + const QList<JarmodPtr> & getJarMods() const; + const QList<LibraryPtr> & getLibraries() const; + QString getMainJarUrl() const; + bool hasTrait(const QString & trait) const; + ProblemSeverity getProblemSeverity() const; + +public: + /// get the profile patch by id + ProfilePatchPtr versionPatch(const QString &id); + + /// get the profile patch by index + ProfilePatchPtr versionPatch(int index); + + /// save the current patch order + void saveCurrentOrder() const; + + /// Remove all the patches + void clearPatches(); + + /// Add the patch object to the internal list of patches + void appendPatch(ProfilePatchPtr patch); + +private: /* data */ + /// the version of Minecraft - jar to use + QString m_minecraftVersion; + + /// Release type - "release" or "snapshot" + QString m_minecraftVersionType; + + /// Assets type - "legacy" or a version ID + MojangAssetIndexInfo::Ptr m_minecraftAssets; + + // Mojang: list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap <QString, std::shared_ptr<MojangDownloadInfo>> mojangDownloads; + + /** + * arguments that should be used for launching minecraft + * + * ex: "--username ${auth_player_name} --session ${auth_session} + * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" + */ + QString m_minecraftArguments; + + /// A list of all tweaker classes + QStringList m_tweakers; + + /// The main class to load first + QString m_mainClass; + + /// The applet class, for some very old minecraft releases + QString m_appletClass; + + /// the list of libraries + QList<LibraryPtr> m_libraries; + + /// traits, collected from all the version files (version files can only add) + QSet<QString> m_traits; + + /// A list of jar mods. version files can add those. + QList<JarmodPtr> m_jarMods; + + ProblemSeverity m_problemSeverity = PROBLEM_NONE; + + /* + FIXME: add support for those rules here? Looks like a pile of quick hacks to me though. + + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx", + "version": "^10\\.5\\.\\d$" + } + } + ], + "incompatibilityReason": "There is a bug in LWJGL which makes it incompatible with OSX + 10.5.8. Please go to New Profile and use 1.5.2 for now. Sorry!" + } + */ + // QList<Rule> rules; + + /// list of attached profile patches + QList<ProfilePatchPtr> m_patches; + + /// strategy used for profile operations + ProfileStrategy *m_strategy = nullptr; +}; diff --git a/api/logic/minecraft/MinecraftVersion.cpp b/api/logic/minecraft/MinecraftVersion.cpp new file mode 100644 index 00000000..1e1d273c --- /dev/null +++ b/api/logic/minecraft/MinecraftVersion.cpp @@ -0,0 +1,215 @@ +#include "MinecraftVersion.h" +#include "MinecraftProfile.h" +#include "VersionBuildError.h" +#include "ProfileUtils.h" +#include "settings/SettingsObject.h" +#include "minecraft/VersionFilterData.h" + +bool MinecraftVersion::usesLegacyLauncher() +{ + return getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; +} + + +QString MinecraftVersion::descriptor() +{ + return m_version; +} + +QString MinecraftVersion::name() +{ + return m_version; +} + +QString MinecraftVersion::typeString() const +{ + if(m_type == "snapshot") + { + return QObject::tr("Snapshot"); + } + else if (m_type == "release") + { + return QObject::tr("Regular release"); + } + else if (m_type == "old_alpha") + { + return QObject::tr("Alpha"); + } + else if (m_type == "old_beta") + { + return QObject::tr("Beta"); + } + else + { + return QString(); + } +} + +VersionSource MinecraftVersion::getVersionSource() +{ + return m_versionSource; +} + +bool MinecraftVersion::hasJarMods() +{ + return false; +} + +bool MinecraftVersion::isMinecraftVersion() +{ + return true; +} + +void MinecraftVersion::applyFileTo(MinecraftProfile *profile) +{ + if(m_versionSource == Local && getVersionFile()) + { + getVersionFile()->applyTo(profile); + } + else + { + throw VersionIncomplete(QObject::tr("Can't apply incomplete/builtin Minecraft version %1").arg(m_version)); + } +} + +QString MinecraftVersion::getUrl() const +{ + // legacy fallback + if(m_versionFileURL.isEmpty()) + { + return QString("http://") + URLConstants::AWS_DOWNLOAD_VERSIONS + m_version + "/" + m_version + ".json"; + } + // current + return m_versionFileURL; +} + +VersionFilePtr MinecraftVersion::getVersionFile() +{ + QFileInfo versionFile(QString("versions/%1/%1.dat").arg(m_version)); + m_problems.clear(); + if(!versionFile.exists()) + { + if(m_loadedVersionFile) + { + m_loadedVersionFile.reset(); + } + addProblem(PROBLEM_WARNING, QObject::tr("The patch file doesn't exist locally. It's possible it just needs to be downloaded.")); + } + else + { + try + { + if(versionFile.lastModified() != m_loadedVersionFileTimestamp) + { + auto loadedVersionFile = ProfileUtils::parseBinaryJsonFile(versionFile); + loadedVersionFile->name = "Minecraft"; + loadedVersionFile->setCustomizable(true); + m_loadedVersionFileTimestamp = versionFile.lastModified(); + m_loadedVersionFile = loadedVersionFile; + } + } + catch(Exception e) + { + m_loadedVersionFile.reset(); + addProblem(PROBLEM_ERROR, QObject::tr("The patch file couldn't be read:\n%1").arg(e.cause())); + } + } + return m_loadedVersionFile; +} + +bool MinecraftVersion::isCustomizable() +{ + switch(m_versionSource) + { + case Local: + case Remote: + // locally cached file, or a remote file that we can acquire can be customized + return true; + default: + // Everything else is undefined and therefore not customizable. + return false; + } + return false; +} + +const QList<PatchProblem> &MinecraftVersion::getProblems() +{ + if(getVersionFile()) + { + return getVersionFile()->getProblems(); + } + return ProfilePatch::getProblems(); +} + +ProblemSeverity MinecraftVersion::getProblemSeverity() +{ + if(getVersionFile()) + { + return getVersionFile()->getProblemSeverity(); + } + return ProfilePatch::getProblemSeverity(); +} + +void MinecraftVersion::applyTo(MinecraftProfile *profile) +{ + // do we have this one cached? + if (m_versionSource == Local) + { + applyFileTo(profile); + return; + } + throw VersionIncomplete(QObject::tr("Minecraft version %1 could not be applied: version files are missing.").arg(m_version)); +} + +int MinecraftVersion::getOrder() +{ + return order; +} + +void MinecraftVersion::setOrder(int order) +{ + this->order = order; +} + +QList<JarmodPtr> MinecraftVersion::getJarMods() +{ + return QList<JarmodPtr>(); +} + +QString MinecraftVersion::getName() +{ + return "Minecraft"; +} +QString MinecraftVersion::getVersion() +{ + return m_version; +} +QString MinecraftVersion::getID() +{ + return "net.minecraft"; +} +QString MinecraftVersion::getFilename() +{ + return QString(); +} +QDateTime MinecraftVersion::getReleaseDateTime() +{ + return m_releaseTime; +} + + +bool MinecraftVersion::needsUpdate() +{ + return m_versionSource == Remote || hasUpdate(); +} + +bool MinecraftVersion::hasUpdate() +{ + return m_versionSource == Remote || (m_versionSource == Local && upstreamUpdate); +} + +bool MinecraftVersion::isCustom() +{ + // if we add any other source types, this will evaluate to false for them. + return m_versionSource != Local && m_versionSource != Remote; +} diff --git a/api/logic/minecraft/MinecraftVersion.h b/api/logic/minecraft/MinecraftVersion.h new file mode 100644 index 00000000..b21427d9 --- /dev/null +++ b/api/logic/minecraft/MinecraftVersion.h @@ -0,0 +1,119 @@ +/* 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 <QStringList> +#include <QSet> +#include <QDateTime> + +#include "BaseVersion.h" +#include "ProfilePatch.h" +#include "VersionFile.h" + +#include "multimc_logic_export.h" + +class MinecraftProfile; +class MinecraftVersion; +typedef std::shared_ptr<MinecraftVersion> MinecraftVersionPtr; + +class MULTIMC_LOGIC_EXPORT MinecraftVersion : public BaseVersion, public ProfilePatch +{ +friend class MinecraftVersionList; + +public: /* methods */ + // FIXME: nuke this. + bool usesLegacyLauncher(); + + virtual QString descriptor() override; + virtual QString name() override; + virtual QString typeString() const override; + virtual bool hasJarMods() override; + virtual bool isMinecraftVersion() override; + virtual void applyTo(MinecraftProfile *profile) override; + virtual int getOrder() override; + virtual void setOrder(int order) override; + virtual QList<JarmodPtr> getJarMods() override; + virtual QString getID() override; + virtual QString getVersion() override; + virtual QString getName() override; + virtual QString getFilename() override; + QDateTime getReleaseDateTime() override; + VersionSource getVersionSource() override; + + bool needsUpdate(); + bool hasUpdate(); + virtual bool isCustom() override; + virtual bool isMoveable() override + { + return false; + } + virtual bool isCustomizable() override; + virtual bool isRemovable() override + { + return false; + } + virtual bool isRevertible() override + { + return false; + } + virtual bool isEditable() override + { + return false; + } + virtual bool isVersionChangeable() override + { + return true; + } + + virtual VersionFilePtr getVersionFile() override; + + // virtual QJsonDocument toJson(bool saveOrder) override; + + QString getUrl() const; + + virtual const QList<PatchProblem> &getProblems() override; + virtual ProblemSeverity getProblemSeverity() override; + +private: /* methods */ + void applyFileTo(MinecraftProfile *profile); + +protected: /* data */ + VersionSource m_versionSource = Remote; + + /// The URL that this version will be downloaded from. + QString m_versionFileURL; + + /// the human readable version name + QString m_version; + + /// The type of this release + QString m_type; + + /// the time this version was actually released by Mojang + QDateTime m_releaseTime; + + /// the time this version was last updated by Mojang + QDateTime m_updateTime; + + /// order of this file... default = -2 + int order = -2; + + /// an update available from Mojang + MinecraftVersionPtr upstreamUpdate; + + QDateTime m_loadedVersionFileTimestamp; + mutable VersionFilePtr m_loadedVersionFile; +}; 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 <QtXml> +#include "Json.h" +#include <QtAlgorithms> +#include <QtNetwork> + +#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 <FileSystem.h> + +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<MinecraftVersion> updatedVersion); + virtual ~MCVListVersionUpdateTask() override{}; + virtual void executeTask() override; + +protected +slots: + void json_downloaded(); + +protected: + NetJobPtr specificVersionDownloadJob; + std::shared_ptr<MinecraftVersion> 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<MinecraftVersion>(first); + auto right = std::dynamic_pointer_cast<MinecraftVersion>(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<BaseVersionPtr> 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<MinecraftVersion> 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<MinecraftVersion>(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<MinecraftVersion>(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<BaseVersionPtr> 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<MinecraftVersion>(m_lookup[descr]); + auto added = std::dynamic_pointer_cast<MinecraftVersion>(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<MinecraftVersion> 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<ByteArrayDownload>(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<Task> MinecraftVersionList::createUpdateTask(QString version) +{ + auto iter = m_lookup.find(version); + if(iter == m_lookup.end()) + return nullptr; + + auto mcversion = std::dynamic_pointer_cast<MinecraftVersion>(*iter); + if(!mcversion) + { + return nullptr; + } + + return std::shared_ptr<Task>(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<MinecraftVersion>(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<MinecraftVersion>(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" diff --git a/api/logic/minecraft/MinecraftVersionList.h b/api/logic/minecraft/MinecraftVersionList.h new file mode 100644 index 00000000..0fca02a7 --- /dev/null +++ b/api/logic/minecraft/MinecraftVersionList.h @@ -0,0 +1,72 @@ +/* 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 <QObject> +#include <QList> +#include <QSet> + +#include "BaseVersionList.h" +#include "tasks/Task.h" +#include "minecraft/MinecraftVersion.h" +#include <net/NetJob.h> + +#include "multimc_logic_export.h" + +class MCVListLoadTask; +class MCVListVersionUpdateTask; + +class MULTIMC_LOGIC_EXPORT MinecraftVersionList : public BaseVersionList +{ + Q_OBJECT +private: + void sortInternal(); + void loadList(QJsonDocument jsonDoc, VersionSource source); + void loadCachedList(); + void saveCachedList(); + void finalizeUpdate(QString version); +public: + friend class MCVListLoadTask; + friend class MCVListVersionUpdateTask; + + explicit MinecraftVersionList(QObject *parent = 0); + + std::shared_ptr<Task> createUpdateTask(QString version); + + 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 QVariant data(const QModelIndex & index, int role) const override; + virtual RoleList providesRoles() const override; + + virtual BaseVersionPtr getLatestStable() const override; + virtual BaseVersionPtr getRecommended() const override; + +protected: + QList<BaseVersionPtr> m_vlist; + QMap<QString, BaseVersionPtr> m_lookup; + + bool m_loaded = false; + bool m_hasLocalIndex = false; + QString m_latestReleaseID = "INVALID"; + QString m_latestSnapshotID = "INVALID"; + +protected +slots: + virtual void updateListData(QList<BaseVersionPtr> versions) override; +}; diff --git a/api/logic/minecraft/Mod.cpp b/api/logic/minecraft/Mod.cpp new file mode 100644 index 00000000..9b9f76f9 --- /dev/null +++ b/api/logic/minecraft/Mod.cpp @@ -0,0 +1,377 @@ +/* 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 <QDir> +#include <QString> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <quazip.h> +#include <quazipfile.h> + +#include "Mod.h" +#include "settings/INIFile.h" +#include <FileSystem.h> +#include <QDebug> + +Mod::Mod(const QFileInfo &file) +{ + repath(file); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + if (m_file.isDir()) + { + m_type = MOD_FOLDER; + m_name = name_base; + m_mmc_id = name_base; + } + else if (m_file.isFile()) + { + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } + else + { + m_enabled = true; + } + m_mmc_id = name_base; + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } + + if (m_type == MOD_ZIPFILE) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); + } + else if (m_type == MOD_FOLDER) + { + QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + ReadMCModInfo(data); + } + } + else if (m_type == MOD_LITEMOD) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); + } +} + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +void Mod::ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->void + { + if (!arr.at(0).isObject()) + return; + auto firstObj = arr.at(0).toObject(); + m_mod_id = firstObj.value("modid").toString(); + m_name = firstObj.value("name").toString(); + m_version = firstObj.value("version").toString(); + m_homeurl = firstObj.value("url").toString(); + m_updateurl = firstObj.value("updateUrl").toString(); + m_homeurl = m_homeurl.trimmed(); + if(!m_homeurl.isEmpty()) + { + // fix up url. + if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") && + !m_homeurl.startsWith("ftp://")) + { + m_homeurl.prepend("http://"); + } + } + m_description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) + authors = firstObj.value("authors").toArray(); + + if (authors.size() == 0) + m_authors = ""; + else if (authors.size() >= 1) + { + m_authors = authors.at(0).toString(); + for (int i = 1; i < authors.size(); i++) + { + m_authors += ", " + authors.at(i).toString(); + } + } + m_credits = firstObj.value("credits").toString(); + return; + } + ; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) + val = jsonDoc.object().value("modListVersion"); + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) + arrVal = jsonDoc.object().value("modList"); + if (arrVal.isArray()) + { + getInfoFromArray(arrVal.toArray()); + } + } +} + +void Mod::ReadForgeInfo(QByteArray contents) +{ + // Read the data + m_name = "Minecraft Forge"; + m_mod_id = "Forge"; + m_homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + m_version = major + "." + minor + "." + revision + "." + build; +} + +void Mod::ReadLiteModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + m_mod_id = m_name = object.value("name").toString(); + } + if (object.contains("version")) + { + m_version = object.value("version").toString(""); + } + else + { + m_version = object.value("revision").toString(""); + } + m_mcversion = object.value("mcversion").toString(); + m_authors = object.value("author").toString(); + m_description = object.value("description").toString(); + m_homeurl = object.value("url").toString(); +} + +bool Mod::replace(Mod &with) +{ + if (!destroy()) + return false; + bool success = false; + auto t = with.type(); + + if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD) + { + qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath(); + success = QFile::copy(with.m_file.filePath(), m_file.filePath()); + } + if (t == MOD_FOLDER) + { + success = FS::copy(with.m_file.filePath(), m_file.path())(); + } + if (success) + { + m_name = with.m_name; + m_mmc_id = with.m_mmc_id; + m_mod_id = with.m_mod_id; + m_version = with.m_version; + m_mcversion = with.m_mcversion; + m_description = with.m_description; + m_authors = with.m_authors; + m_credits = with.m_credits; + m_homeurl = with.m_homeurl; + m_type = with.m_type; + m_file.refresh(); + } + return success; +} + +bool Mod::destroy() +{ + if (m_type == MOD_FOLDER) + { + QDir d(m_file.filePath()); + if (d.removeRecursively()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) + { + QFile f(m_file.filePath()); + if (f.remove()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + return true; +} + +QString Mod::version() const +{ + switch (type()) + { + case MOD_ZIPFILE: + case MOD_LITEMOD: + return m_version; + case MOD_FOLDER: + return "Folder"; + case MOD_SINGLEFILE: + return "File"; + default: + return "VOID"; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + m_file = QFileInfo(path); + m_enabled = value; + return true; +} +bool Mod::operator==(const Mod &other) const +{ + return mmc_id() == other.mmc_id(); +} +bool Mod::strongCompare(const Mod &other) const +{ + return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type(); +} diff --git a/api/logic/minecraft/Mod.h b/api/logic/minecraft/Mod.h new file mode 100644 index 00000000..19f4c740 --- /dev/null +++ b/api/logic/minecraft/Mod.h @@ -0,0 +1,134 @@ +/* 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 <QFileInfo> + +class Mod +{ +public: + enum ModType + { + MOD_UNKNOWN, //!< Indicates an unspecified mod type. + MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. + MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). + MOD_FOLDER, //!< The mod is in a folder on the filesystem. + MOD_LITEMOD, //!< The mod is a litemod + }; + + Mod(const QFileInfo &file); + + QFileInfo filename() const + { + return m_file; + } + QString mmc_id() const + { + return m_mmc_id; + } + QString mod_id() const + { + return m_mod_id; + } + ModType type() const + { + return m_type; + } + QString mcversion() const + { + return m_mcversion; + } + ; + bool valid() + { + return m_type != MOD_UNKNOWN; + } + QString name() const + { + if(m_name.trimmed().isEmpty()) + { + return m_mmc_id; + } + return m_name; + } + + QString version() const; + + QString homeurl() const + { + return m_homeurl; + } + + QString description() const + { + return m_description; + } + + QString authors() const + { + return m_authors; + } + + QString credits() const + { + return m_credits; + } + + bool enabled() const + { + return m_enabled; + } + + bool enable(bool value); + + // delete all the files of this mod + bool destroy(); + // replace this mod with a copy of the other + bool replace(Mod &with); + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + // WEAK compare operator - used for replacing mods + bool operator==(const Mod &other) const; + bool strongCompare(const Mod &other) const; + +private: + void ReadMCModInfo(QByteArray contents); + void ReadForgeInfo(QByteArray contents); + void ReadLiteModInfo(QByteArray contents); + +protected: + + // FIXME: what do do with those? HMM... + /* + void ReadModInfoData(QString info); + void ReadForgeInfoData(QString infoFileData); + */ + + QFileInfo m_file; + QString m_mmc_id; + QString m_mod_id; + bool m_enabled = true; + QString m_name; + QString m_version; + QString m_mcversion; + QString m_homeurl; + QString m_updateurl; + QString m_description; + QString m_authors; + QString m_credits; + + ModType m_type; +}; diff --git a/api/logic/minecraft/ModList.cpp b/api/logic/minecraft/ModList.cpp new file mode 100644 index 00000000..d9ed4886 --- /dev/null +++ b/api/logic/minecraft/ModList.cpp @@ -0,0 +1,616 @@ +/* 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 "ModList.h" +#include <FileSystem.h> +#include <QMimeData> +#include <QUrl> +#include <QUuid> +#include <QString> +#include <QFileSystemWatcher> +#include <QDebug> + +ModList::ModList(const QString &dir, const QString &list_file) + : QAbstractListModel(), m_dir(dir), m_list_file(list_file) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_list_id = QUuid::createUuid().toString(); + m_watcher = new QFileSystemWatcher(this); + is_watching = false; + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void ModList::startWatching() +{ + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void ModList::stopWatching() +{ + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +void ModList::internalSort(QList<Mod> &what) +{ + auto predicate = [](const Mod &left, const Mod &right) + { + if (left.name() == right.name()) + { + return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0; + } + return left.name().localeAwareCompare(right.name()) < 0; + }; + std::sort(what.begin(), what.end(), predicate); +} + +bool ModList::update() +{ + if (!isValid()) + return false; + + QList<Mod> orderedMods; + QList<Mod> newMods; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + bool orderOrStateChanged = false; + + // first, process the ordered items (if any) + OrderList listOrder = readListFile(); + for (auto item : listOrder) + { + QFileInfo infoEnabled(m_dir.filePath(item.id)); + QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled")); + int idxEnabled = folderContents.indexOf(infoEnabled); + int idxDisabled = folderContents.indexOf(infoDisabled); + bool isEnabled; + // if both enabled and disabled versions are present, it's a special case... + if (idxEnabled >= 0 && idxDisabled >= 0) + { + // we only process the one we actually have in the order file. + // and exactly as we have it. + // THIS IS A CORNER CASE + isEnabled = item.enabled; + } + else + { + // only one is present. + // we pick the one that we found. + // we assume the mod was enabled/disabled by external means + isEnabled = idxEnabled >= 0; + } + int idx = isEnabled ? idxEnabled : idxDisabled; + QFileInfo &info = isEnabled ? infoEnabled : infoDisabled; + // if the file from the index file exists + if (idx != -1) + { + // remove from the actual folder contents list + folderContents.takeAt(idx); + // append the new mod + orderedMods.append(Mod(info)); + if (isEnabled != item.enabled) + orderOrStateChanged = true; + } + else + { + orderOrStateChanged = true; + } + } + // if there are any untracked files... + if (folderContents.size()) + { + // the order surely changed! + for (auto entry : folderContents) + { + newMods.append(Mod(entry)); + } + internalSort(newMods); + orderedMods.append(newMods); + orderOrStateChanged = true; + } + // otherwise, if we were already tracking some mods + else if (mods.size()) + { + // if the number doesn't match, order changed. + if (mods.size() != orderedMods.size()) + orderOrStateChanged = true; + // if it does match, compare the mods themselves + else + for (int i = 0; i < mods.size(); i++) + { + if (!mods[i].strongCompare(orderedMods[i])) + { + orderOrStateChanged = true; + break; + } + } + } + beginResetModel(); + mods.swap(orderedMods); + endResetModel(); + if (orderOrStateChanged && !m_list_file.isEmpty()) + { + qDebug() << "Mod list " << m_list_file << " changed!"; + saveListFile(); + emit changed(); + } + return true; +} + +void ModList::directoryChanged(QString path) +{ + update(); +} + +ModList::OrderList ModList::readListFile() +{ + OrderList itemList; + if (m_list_file.isNull() || m_list_file.isEmpty()) + return itemList; + + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return OrderList(); + + QTextStream textStream; + textStream.setAutoDetectUnicode(true); + textStream.setDevice(&textFile); + while (true) + { + QString line = textStream.readLine(); + if (line.isNull() || line.isEmpty()) + break; + else + { + OrderItem it; + it.enabled = !line.endsWith(".disabled"); + if (!it.enabled) + { + line.chop(9); + } + it.id = line; + itemList.append(it); + } + } + textFile.close(); + return itemList; +} + +bool ModList::saveListFile() +{ + if (m_list_file.isNull() || m_list_file.isEmpty()) + return false; + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + return false; + QTextStream textStream; + textStream.setGenerateByteOrderMark(true); + textStream.setCodec("UTF-8"); + textStream.setDevice(&textFile); + for (auto mod : mods) + { + textStream << mod.mmc_id(); + if (!mod.enabled()) + textStream << ".disabled"; + textStream << endl; + } + textFile.close(); + return false; +} + +bool ModList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool ModList::installMod(const QString &filename, int index) +{ + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + QFileInfo fileinfo(FS::NormalizePath(filename)); + + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + if (!fileinfo.exists() || !fileinfo.isReadable() || index < 0) + { + return false; + } + Mod m(fileinfo); + if (!m.valid()) + return false; + + // if it's already there, replace the original mod (in place) + int idx = mods.indexOf(m); + if (idx != -1) + { + int idx2 = mods.indexOf(m, idx + 1); + if (idx2 != -1) + return false; + if (mods[idx].replace(m)) + { + + auto left = this->index(index); + auto right = this->index(index, columnCount(QModelIndex()) - 1); + emit dataChanged(left, right); + saveListFile(); + update(); + return true; + } + return false; + } + + auto type = m.type(); + if (type == Mod::MOD_UNKNOWN) + return false; + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName()); + if (!QFile::copy(fileinfo.filePath(), newpath)) + return false; + m.repath(newpath); + beginInsertRows(QModelIndex(), index, index); + mods.insert(index, m); + endInsertRows(); + saveListFile(); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + + QString from = fileinfo.filePath(); + QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName()); + if (!FS::copy(from, to)()) + return false; + m.repath(to); + beginInsertRows(QModelIndex(), index, index); + mods.insert(index, m); + endInsertRows(); + saveListFile(); + update(); + return true; + } + return false; +} + +bool ModList::deleteMod(int index) +{ + if (index >= mods.size() || index < 0) + return false; + Mod &m = mods[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + mods.removeAt(index); + endRemoveRows(); + saveListFile(); + emit changed(); + return true; + } + return false; +} + +bool ModList::deleteMods(int first, int last) +{ + for (int i = first; i <= last; i++) + { + Mod &m = mods[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + mods.erase(mods.begin() + first, mods.begin() + last + 1); + endRemoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModTo(int from, int to) +{ + if (from < 0 || from >= mods.size()) + return false; + if (to >= rowCount()) + to = rowCount() - 1; + if (to == -1) + to = rowCount() - 1; + if (from == to) + return false; + int togap = to > from ? to + 1 : to; + beginMoveRows(QModelIndex(), from, from, QModelIndex(), togap); + mods.move(from, to); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModUp(int from) +{ + if (from > 0) + return moveModTo(from, from - 1); + return false; +} + +bool ModList::moveModsUp(int first, int last) +{ + if (first == 0) + return false; + + beginMoveRows(QModelIndex(), first, last, QModelIndex(), first - 1); + mods.move(first - 1, last); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModDown(int from) +{ + if (from < 0) + return false; + if (from < mods.size() - 1) + return moveModTo(from, from + 1); + return false; +} + +bool ModList::moveModsDown(int first, int last) +{ + if (last == mods.size() - 1) + return false; + + beginMoveRows(QModelIndex(), first, last, QModelIndex(), last + 2); + mods.move(last + 1, first); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +int ModList::columnCount(const QModelIndex &parent) const +{ + return 3; +} + +QVariant ModList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= mods.size()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return mods[row].name(); + case VersionColumn: + return mods[row].version(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto &mod = mods[index.row()]; + if (mod.enable(!mod.enabled())) + { + emit dataChanged(index, index); + return true; + } + } + return false; +} + +QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +QStringList ModList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + types << "text/plain"; + return types; +} + +Qt::DropActions ModList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::DropActions ModList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +QMimeData *ModList::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = new QMimeData(); + + if (indexes.size() == 0) + return data; + + auto idx = indexes[0]; + int row = idx.row(); + if (row < 0 || row >= mods.size()) + return data; + + QStringList params; + params << m_list_id << QString::number(row); + data->setText(params.join('|')); + return data; +} + +bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + if (parent.isValid()) + { + row = parent.row(); + column = parent.column(); + } + + if (row > rowCount()) + row = rowCount(); + if (row == -1) + row = rowCount(); + if (column == -1) + column = 0; + qDebug() << "Drop row: " << row << " column: " << column; + + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = is_watching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + installMod(filename, row); + // if there is no ordering, re-sort the list + if (m_list_file.isEmpty()) + { + beginResetModel(); + internalSort(mods); + endResetModel(); + } + } + if (was_watching) + startWatching(); + return true; + } + else if (data->hasText()) + { + QString sourcestr = data->text(); + auto list = sourcestr.split('|'); + if (list.size() != 2) + return false; + QString remoteId = list[0]; + int remoteIndex = list[1].toInt(); + qDebug() << "move: " << sourcestr; + // no moving of things between two lists + if (remoteId != m_list_id) + return false; + // no point moving to the same place... + if (row == remoteIndex) + return false; + // otherwise, move the mod :D + moveModTo(remoteIndex, row); + return true; + } + return false; +} diff --git a/api/logic/minecraft/ModList.h b/api/logic/minecraft/ModList.h new file mode 100644 index 00000000..05ada8ee --- /dev/null +++ b/api/logic/minecraft/ModList.h @@ -0,0 +1,160 @@ +/* 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 <QList> +#include <QString> +#include <QDir> +#include <QAbstractListModel> + +#include "minecraft/Mod.h" + +#include "multimc_logic_export.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class MULTIMC_LOGIC_EXPORT ModList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + VersionColumn + }; + ModList(const QString &dir, const QString &list_file = QString()); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return size(); + } + ; + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex &parent) const; + + size_t size() const + { + return mods.size(); + } + ; + bool empty() const + { + return size() == 0; + } + Mod &operator[](size_t index) + { + return mods[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + virtual bool installMod(const QString & filename, int index = 0); + + /// Deletes the mod at the given index. + virtual bool deleteMod(int index); + + /// Deletes all the selected mods + virtual bool deleteMods(int first, int last); + + /** + * move the mod at index to the position N + * 0 is the beginning of the list, length() is the end of the list. + */ + virtual bool moveModTo(int from, int to); + + /** + * move the mod at index one position upwards + */ + virtual bool moveModUp(int from); + virtual bool moveModsUp(int first, int last); + + /** + * move the mod at index one position downwards + */ + virtual bool moveModDown(int from); + virtual bool moveModsDown(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + /// get data for drag action + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() + { + return m_dir; + } + + const QList<Mod> & allMods() + { + return mods; + } + +private: + void internalSort(QList<Mod> & what); + struct OrderItem + { + QString id; + bool enabled = false; + }; + typedef QList<OrderItem> OrderList; + OrderList readListFile(); + bool saveListFile(); +private +slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching; + QDir m_dir; + QString m_list_file; + QString m_list_id; + QList<Mod> mods; +}; diff --git a/api/logic/minecraft/MojangDownloadInfo.h b/api/logic/minecraft/MojangDownloadInfo.h new file mode 100644 index 00000000..1f3306e0 --- /dev/null +++ b/api/logic/minecraft/MojangDownloadInfo.h @@ -0,0 +1,71 @@ +#pragma once +#include <QString> +#include <QMap> +#include <memory> + +struct MojangDownloadInfo +{ + // types + typedef std::shared_ptr<MojangDownloadInfo> Ptr; + + // data + /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! + QString path; + /// absolute URL of this file + QString url; + /// sha-1 checksum of the file + QString sha1; + /// size of the file in bytes + int size; +}; + + + +struct MojangLibraryDownloadInfo +{ + MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact): artifact(artifact) {}; + MojangLibraryDownloadInfo() {}; + + // types + typedef std::shared_ptr<MojangLibraryDownloadInfo> Ptr; + + // methods + MojangDownloadInfo *getDownloadInfo(QString classifier) + { + if (classifier.isNull()) + { + return artifact.get(); + } + + return classifiers[classifier].get(); + } + + // data + MojangDownloadInfo::Ptr artifact; + QMap<QString, MojangDownloadInfo::Ptr> classifiers; +}; + + + +struct MojangAssetIndexInfo : public MojangDownloadInfo +{ + // types + typedef std::shared_ptr<MojangAssetIndexInfo> Ptr; + + // methods + MojangAssetIndexInfo() + { + } + + MojangAssetIndexInfo(QString id) + { + this->id = id; + url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id + ".json"; + known = false; + } + + // data + int totalSize; + QString id; + bool known = true; +}; diff --git a/api/logic/minecraft/MojangVersionFormat.cpp b/api/logic/minecraft/MojangVersionFormat.cpp new file mode 100644 index 00000000..34129c9e --- /dev/null +++ b/api/logic/minecraft/MojangVersionFormat.cpp @@ -0,0 +1,381 @@ +#include "MojangVersionFormat.h" +#include "onesix/OneSixVersionFormat.h" +#include "MinecraftVersion.h" +#include "VersionBuildError.h" +#include "MojangDownloadInfo.h" + +#include "Json.h" +using namespace Json; +#include "ParseUtils.h" + +static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; + +static MojangAssetIndexInfo::Ptr assetIndexFromJson (const QJsonObject &obj); +static MojangDownloadInfo::Ptr downloadInfoFromJson (const QJsonObject &obj); +static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson (const QJsonObject &libObj); +static QJsonObject assetIndexToJson (MojangAssetIndexInfo::Ptr assetidxinfo); +static QJsonObject libDownloadInfoToJson (MojangLibraryDownloadInfo::Ptr libinfo); +static QJsonObject downloadInfoToJson (MojangDownloadInfo::Ptr info); + +namespace Bits +{ +static void readString(const QJsonObject &root, const QString &key, QString &variable) +{ + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } +} + +static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject &obj) +{ + // optional, not used + readString(obj, "path", out->path); + // required! + out->sha1 = requireString(obj, "sha1"); + out->url = requireString(obj, "url"); + out->size = requireInteger(obj, "size"); +} + +static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject &obj) +{ + out->totalSize = requireInteger(obj, "totalSize"); + out->id = requireString(obj, "id"); + // out->known = true; +} +} + +MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject &obj) +{ + auto out = std::make_shared<MojangDownloadInfo>(); + Bits::readDownloadInfo(out, obj); + return out; +} + +MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject &obj) +{ + auto out = std::make_shared<MojangAssetIndexInfo>(); + Bits::readDownloadInfo(out, obj); + Bits::readAssetIndex(out, obj); + return out; +} + +QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info) +{ + QJsonObject out; + if(!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + return out; +} + +MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject &libObj) +{ + auto out = std::make_shared<MojangLibraryDownloadInfo>(); + auto dlObj = requireObject(libObj.value("downloads")); + if(dlObj.contains("artifact")) + { + out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); + } + if(dlObj.contains("classifiers")) + { + auto classifiersObj = requireObject(dlObj, "classifiers"); + for(auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->classifiers[classifier] = downloadInfoFromJson(classifierObj); + } + } + return out; +} + +QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) +{ + QJsonObject out; + if(libinfo->artifact) + { + out.insert("artifact", downloadInfoToJson(libinfo->artifact)); + } + if(libinfo->classifiers.size()) + { + QJsonObject classifiersOut; + for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) + { + classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("classifiers", classifiersOut); + } + return out; +} + +QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) +{ + QJsonObject out; + if(!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + out.insert("totalSize", info->totalSize); + out.insert("id", info->id); + return out; +} + +void MojangVersionFormat::readVersionProperties(const QJsonObject &in, VersionFile *out) +{ + Bits::readString(in, "id", out->minecraftVersion); + Bits::readString(in, "mainClass", out->mainClass); + Bits::readString(in, "minecraftArguments", out->minecraftArguments); + if(out->minecraftArguments.isEmpty()) + { + QString processArguments; + Bits::readString(in, "processArguments", processArguments); + QString toCompare = processArguments.toLower(); + if (toCompare == "legacy") + { + out->minecraftArguments = " ${auth_player_name} ${auth_session}"; + } + else if (toCompare == "username_session") + { + out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}"; + } + else if (toCompare == "username_session_version") + { + out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}"; + } + else if (!toCompare.isEmpty()) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments)); + } + } + Bits::readString(in, "type", out->type); + + Bits::readString(in, "assets", out->assets); + if(in.contains("assetIndex")) + { + out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); + } + else if (!out->assets.isNull()) + { + out->mojangAssetIndex = std::make_shared<MojangAssetIndexInfo>(out->assets); + } + + out->m_releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); + out->m_updateTime = timeFromS3Time(in.value("time").toString("")); + + if (in.contains("minimumLauncherVersion")) + { + out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion")); + if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) + { + out->addProblem( + PROBLEM_WARNING, + QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than supported by MultiMC (%2). It might not work properly!") + .arg(out->minimumLauncherVersion) + .arg(CURRENT_MINIMUM_LAUNCHER_VERSION)); + } + } + if(in.contains("downloads")) + { + auto downloadsObj = requireObject(in, "downloads"); + for(auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); + } + } +} + +VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + readVersionProperties(root, out.get()); + + out->name = "Minecraft"; + out->fileId = "net.minecraft"; + out->version = out->minecraftVersion; + out->filename = filename; + + + if (root.contains("libraries")) + { + for (auto libVal : requireArray(root.value("libraries"))) + { + auto libObj = requireObject(libVal); + + auto lib = MojangVersionFormat::libraryFromJson(libObj, filename); + out->libraries.append(lib); + } + } + return out; +} + +void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) +{ + writeString(out, "id", in->minecraftVersion); + writeString(out, "mainClass", in->mainClass); + writeString(out, "minecraftArguments", in->minecraftArguments); + writeString(out, "type", in->type); + if(!in->m_releaseTime.isNull()) + { + writeString(out, "releaseTime", timeToS3Time(in->m_releaseTime)); + } + if(!in->m_updateTime.isNull()) + { + writeString(out, "time", timeToS3Time(in->m_updateTime)); + } + if(in->minimumLauncherVersion != -1) + { + out.insert("minimumLauncherVersion", in->minimumLauncherVersion); + } + writeString(out, "assets", in->assets); + if(in->mojangAssetIndex && in->mojangAssetIndex->known) + { + out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); + } + if(in->mojangDownloads.size()) + { + QJsonObject downloadsOut; + for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) + { + downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("downloads", downloadsOut); + } +} + +QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch) +{ + QJsonObject root; + writeVersionProperties(patch.get(), root); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value: patch->libraries) + { + array.append(MojangVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); + } + out->m_name = libObj.value("name").toString(); + + Bits::readString(libObj, "url", out->m_repositoryURL); + if (libObj.contains("extract")) + { + out->m_hasExcludes = true; + auto extractObj = requireObject(libObj.value("extract")); + for (auto excludeVal : requireArray(extractObj.value("exclude"))) + { + out->m_extractExcludes.append(requireString(excludeVal)); + } + } + if (libObj.contains("natives")) + { + QJsonObject nativesObj = requireObject(libObj.value("natives")); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + qWarning() << filename << "contains an invalid native (skipping)"; + } + OpSys opSys = OpSys_fromString(it.key()); + if (opSys != Os_Other) + { + out->m_nativeClassifiers[opSys] = it.value().toString(); + } + } + } + if (libObj.contains("rules")) + { + out->applyRules = true; + out->m_rules = rulesFromJsonV4(libObj); + } + if (libObj.contains("downloads")) + { + out->m_mojangDownloads = libDownloadInfoFromJson(libObj); + } + return out; +} + +QJsonObject MojangVersionFormat::libraryToJson(Library *library) +{ + QJsonObject libRoot; + libRoot.insert("name", (QString)library->m_name); + if (!library->m_repositoryURL.isEmpty()) + { + libRoot.insert("url", library->m_repositoryURL); + } + if (library->isNative()) + { + QJsonObject nativeList; + auto iter = library->m_nativeClassifiers.begin(); + while (iter != library->m_nativeClassifiers.end()) + { + nativeList.insert(OpSys_toString(iter.key()), iter.value()); + iter++; + } + libRoot.insert("natives", nativeList); + if (library->m_extractExcludes.size()) + { + QJsonArray excludes; + QJsonObject extract; + for (auto exclude : library->m_extractExcludes) + { + excludes.append(exclude); + } + extract.insert("exclude", excludes); + libRoot.insert("extract", extract); + } + } + if (library->m_rules.size()) + { + QJsonArray allRules; + for (auto &rule : library->m_rules) + { + QJsonObject ruleObj = rule->toJson(); + allRules.append(ruleObj); + } + libRoot.insert("rules", allRules); + } + if(library->m_mojangDownloads) + { + auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); + libRoot.insert("downloads", downloadsObj); + } + return libRoot; +} diff --git a/api/logic/minecraft/MojangVersionFormat.h b/api/logic/minecraft/MojangVersionFormat.h new file mode 100644 index 00000000..4e141088 --- /dev/null +++ b/api/logic/minecraft/MojangVersionFormat.h @@ -0,0 +1,25 @@ +#pragma once + +#include <minecraft/VersionFile.h> +#include <minecraft/Library.h> +#include <QJsonDocument> + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT MojangVersionFormat +{ +friend class OneSixVersionFormat; +protected: + // does not include libraries + static void readVersionProperties(const QJsonObject& in, VersionFile* out); + // does not include libraries + static void writeVersionProperties(const VersionFile* in, QJsonObject& out); +public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename); + static QJsonDocument versionFileToJson(const VersionFilePtr &patch); + + // libraries + static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static QJsonObject libraryToJson(Library *library); +}; diff --git a/api/logic/minecraft/OpSys.cpp b/api/logic/minecraft/OpSys.cpp new file mode 100644 index 00000000..4c2a236d --- /dev/null +++ b/api/logic/minecraft/OpSys.cpp @@ -0,0 +1,42 @@ +/* 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 "OpSys.h" + +OpSys OpSys_fromString(QString name) +{ + if (name == "linux") + return Os_Linux; + if (name == "windows") + return Os_Windows; + if (name == "osx") + return Os_OSX; + return Os_Other; +} + +QString OpSys_toString(OpSys name) +{ + switch (name) + { + case Os_Linux: + return "linux"; + case Os_OSX: + return "osx"; + case Os_Windows: + return "windows"; + default: + return "other"; + } +}
\ No newline at end of file diff --git a/api/logic/minecraft/OpSys.h b/api/logic/minecraft/OpSys.h new file mode 100644 index 00000000..9ebea3de --- /dev/null +++ b/api/logic/minecraft/OpSys.h @@ -0,0 +1,37 @@ +/* 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 <QString> +enum OpSys +{ + Os_Windows, + Os_Linux, + Os_OSX, + Os_Other +}; + +OpSys OpSys_fromString(QString); +QString OpSys_toString(OpSys); + +#ifdef Q_OS_WIN32 +#define currentSystem Os_Windows +#else +#ifdef Q_OS_MAC +#define currentSystem Os_OSX +#else +#define currentSystem Os_Linux +#endif +#endif
\ No newline at end of file diff --git a/api/logic/minecraft/ParseUtils.cpp b/api/logic/minecraft/ParseUtils.cpp new file mode 100644 index 00000000..ca188432 --- /dev/null +++ b/api/logic/minecraft/ParseUtils.cpp @@ -0,0 +1,34 @@ +#include <QDateTime> +#include <QString> +#include "ParseUtils.h" +#include <QDebug> +#include <cstdlib> + +QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + +QString timeToS3Time(QDateTime time) +{ + // this all because Qt can't format timestamps right. + int offsetRaw = time.offsetFromUtc(); + bool negative = offsetRaw < 0; + int offsetAbs = std::abs(offsetRaw); + + int offsetSeconds = offsetAbs % 60; + offsetAbs -= offsetSeconds; + + int offsetMinutes = offsetAbs % 3600; + offsetAbs -= offsetMinutes; + offsetMinutes /= 60; + + int offsetHours = offsetAbs / 3600; + + QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); + raw += (negative ? QChar('-') : QChar('+')); + raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); + raw += ":"; + raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); + return raw; +} diff --git a/api/logic/minecraft/ParseUtils.h b/api/logic/minecraft/ParseUtils.h new file mode 100644 index 00000000..2b367a10 --- /dev/null +++ b/api/logic/minecraft/ParseUtils.h @@ -0,0 +1,11 @@ +#pragma once +#include <QString> +#include <QDateTime> + +#include "multimc_logic_export.h" + +/// take the timestamp used by S3 and turn it into QDateTime +MULTIMC_LOGIC_EXPORT QDateTime timeFromS3Time(QString str); + +/// take a timestamp and convert it into an S3 timestamp +MULTIMC_LOGIC_EXPORT QString timeToS3Time(QDateTime); diff --git a/api/logic/minecraft/ProfilePatch.h b/api/logic/minecraft/ProfilePatch.h new file mode 100644 index 00000000..f0c65360 --- /dev/null +++ b/api/logic/minecraft/ProfilePatch.h @@ -0,0 +1,104 @@ +#pragma once + +#include <memory> +#include <QList> +#include <QJsonDocument> +#include <QDateTime> +#include "JarMod.h" + +class MinecraftProfile; + +enum ProblemSeverity +{ + PROBLEM_NONE, + PROBLEM_WARNING, + PROBLEM_ERROR +}; + +/// where is a version from? +enum VersionSource +{ + Local, //!< version loaded from a file in the cache. + Remote, //!< incomplete version on a remote server. +}; + +class PatchProblem +{ +public: + PatchProblem(ProblemSeverity severity, const QString & description) + { + m_severity = severity; + m_description = description; + } + const QString & getDescription() const + { + return m_description; + } + const ProblemSeverity getSeverity() const + { + return m_severity; + } +private: + ProblemSeverity m_severity; + QString m_description; +}; + +class ProfilePatch : public std::enable_shared_from_this<ProfilePatch> +{ +public: + virtual ~ProfilePatch(){}; + virtual void applyTo(MinecraftProfile *profile) = 0; + + virtual bool isMinecraftVersion() = 0; + virtual bool hasJarMods() = 0; + virtual QList<JarmodPtr> getJarMods() = 0; + + virtual bool isMoveable() = 0; + virtual bool isCustomizable() = 0; + virtual bool isRevertible() = 0; + virtual bool isRemovable() = 0; + virtual bool isCustom() = 0; + virtual bool isEditable() = 0; + virtual bool isVersionChangeable() = 0; + + virtual void setOrder(int order) = 0; + virtual int getOrder() = 0; + + virtual QString getID() = 0; + virtual QString getName() = 0; + virtual QString getVersion() = 0; + virtual QDateTime getReleaseDateTime() = 0; + + virtual QString getFilename() = 0; + + virtual VersionSource getVersionSource() = 0; + + virtual std::shared_ptr<class VersionFile> getVersionFile() = 0; + + virtual const QList<PatchProblem>& getProblems() + { + return m_problems; + } + virtual void addProblem(ProblemSeverity severity, const QString &description) + { + if(severity > m_problemSeverity) + { + m_problemSeverity = severity; + } + m_problems.append(PatchProblem(severity, description)); + } + virtual ProblemSeverity getProblemSeverity() + { + return m_problemSeverity; + } + virtual bool hasFailed() + { + return getProblemSeverity() == PROBLEM_ERROR; + } + +protected: + QList<PatchProblem> m_problems; + ProblemSeverity m_problemSeverity = PROBLEM_NONE; +}; + +typedef std::shared_ptr<ProfilePatch> ProfilePatchPtr; diff --git a/api/logic/minecraft/ProfileStrategy.h b/api/logic/minecraft/ProfileStrategy.h new file mode 100644 index 00000000..b4dfc4b3 --- /dev/null +++ b/api/logic/minecraft/ProfileStrategy.h @@ -0,0 +1,35 @@ +#pragma once + +#include "ProfileUtils.h" + +class MinecraftProfile; + +class ProfileStrategy +{ + friend class MinecraftProfile; +public: + virtual ~ProfileStrategy(){}; + + /// load the patch files into the profile + virtual void load() = 0; + + /// reset the order of patches + virtual bool resetOrder() = 0; + + /// save the order of patches, given the order + virtual bool saveOrder(ProfileUtils::PatchOrder order) = 0; + + /// install a list of jar mods into the instance + virtual bool installJarMods(QStringList filepaths) = 0; + + /// remove any files or records that constitute the version patch + virtual bool removePatch(ProfilePatchPtr jarMod) = 0; + + /// make the patch custom, if possible + virtual bool customizePatch(ProfilePatchPtr patch) = 0; + + /// revert the custom patch to 'vanilla', if possible + virtual bool revertPatch(ProfilePatchPtr patch) = 0; +protected: + MinecraftProfile *profile; +}; diff --git a/api/logic/minecraft/ProfileUtils.cpp b/api/logic/minecraft/ProfileUtils.cpp new file mode 100644 index 00000000..ef9b3b28 --- /dev/null +++ b/api/logic/minecraft/ProfileUtils.cpp @@ -0,0 +1,191 @@ +#include "ProfileUtils.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/onesix/OneSixVersionFormat.h" +#include "Json.h" +#include <QDebug> + +#include <QJsonDocument> +#include <QJsonArray> +#include <QRegularExpression> +#include <QSaveFile> + +namespace ProfileUtils +{ + +static const int currentOrderFileVersion = 1; + +bool writeOverrideOrders(QString path, const PatchOrder &order) +{ + QJsonObject obj; + obj.insert("version", currentOrderFileVersion); + QJsonArray orderArray; + for(auto str: order) + { + orderArray.append(str); + } + obj.insert("order", orderArray); + QSaveFile orderFile(path); + if (!orderFile.open(QFile::WriteOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() + << "for writing:" << orderFile.errorString(); + return false; + } + auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); + if(orderFile.write(data) != data.size()) + { + qCritical() << "Couldn't write all the data into" << orderFile.fileName() + << "because:" << orderFile.errorString(); + return false; + } + if(!orderFile.commit()) + { + qCritical() << "Couldn't save" << orderFile.fileName() + << "because:" << orderFile.errorString(); + } + return true; +} + +bool readOverrideOrders(QString path, PatchOrder &order) +{ + QFile orderFile(path); + if (!orderFile.exists()) + { + qWarning() << "Order file doesn't exist. Ignoring."; + return false; + } + if (!orderFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() + << " for reading:" << orderFile.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("version")); + if (version != currentOrderFileVersion) + { + throw JSONValidationError(QObject::tr("Invalid order file version, expected %1") + .arg(currentOrderFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("order")); + for(auto item: orderArray) + { + order.append(Json::requireString(item)); + } + } + catch (JSONValidationError &err) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; + qWarning() << "Ignoring overriden order"; + order.clear(); + return false; + } + return true; +} + +static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) +{ + auto outError = std::make_shared<VersionFile>(); + outError->fileId = outError->name = fileId; + outError->filename = filepath; + outError->addProblem(PROBLEM_ERROR, error); + return outError; +} + +static VersionFilePtr guardedParseJson(const QJsonDocument & doc,const QString &fileId,const QString &filepath,const bool &requireOrder) +{ + try + { + return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); + } + catch (Exception & e) + { + return createErrorVersionFile(fileId, filepath, e.cause()); + } +} + +VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonParseError error; + auto data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + file.close(); + if (error.error != QJsonParseError::NoError) + { + int line = 1; + int column = 0; + for(int i = 0; i < error.offset; i++) + { + if(data[i] == '\n') + { + line++; + column = 0; + continue; + } + column++; + } + auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") + .arg(fileInfo.fileName(), error.errorString()) + .arg(line).arg(column); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); +} + +VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonDocument doc = QJsonDocument::fromBinaryData(file.readAll()); + file.close(); + if (doc.isNull()) + { + file.remove(); + throw JSONValidationError(QObject::tr("Unable to process the version file %1.").arg(fileInfo.fileName())); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), false); +} + +void removeLwjglFromPatch(VersionFilePtr patch) +{ + auto filter = [](QList<LibraryPtr>& libs) + { + QList<LibraryPtr> filteredLibs; + for (auto lib : libs) + { + if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + { + filteredLibs.append(lib); + } + } + libs = filteredLibs; + }; + filter(patch->libraries); +} +} diff --git a/api/logic/minecraft/ProfileUtils.h b/api/logic/minecraft/ProfileUtils.h new file mode 100644 index 00000000..267fd42b --- /dev/null +++ b/api/logic/minecraft/ProfileUtils.h @@ -0,0 +1,25 @@ +#pragma once +#include "Library.h" +#include "VersionFile.h" + +namespace ProfileUtils +{ +typedef QStringList PatchOrder; + +/// Read and parse a OneSix format order file +bool readOverrideOrders(QString path, PatchOrder &order); + +/// Write a OneSix format order file +bool writeOverrideOrders(QString path, const PatchOrder &order); + + +/// Parse a version file in JSON format +VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder); + +/// Parse a version file in binary JSON format +VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo); + +/// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. +void removeLwjglFromPatch(VersionFilePtr patch); + +} diff --git a/api/logic/minecraft/Rule.cpp b/api/logic/minecraft/Rule.cpp new file mode 100644 index 00000000..c8ba297b --- /dev/null +++ b/api/logic/minecraft/Rule.cpp @@ -0,0 +1,93 @@ +/* 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 <QJsonObject> +#include <QJsonArray> + +#include "Rule.h" + +RuleAction RuleAction_fromString(QString name) +{ + if (name == "allow") + return Allow; + if (name == "disallow") + return Disallow; + return Defer; +} + +QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules) +{ + QList<std::shared_ptr<Rule>> rules; + auto rulesVal = objectWithRules.value("rules"); + if (!rulesVal.isArray()) + return rules; + + QJsonArray ruleList = rulesVal.toArray(); + for (auto ruleVal : ruleList) + { + std::shared_ptr<Rule> rule; + if (!ruleVal.isObject()) + continue; + auto ruleObj = ruleVal.toObject(); + auto actionVal = ruleObj.value("action"); + if (!actionVal.isString()) + continue; + auto action = RuleAction_fromString(actionVal.toString()); + if (action == Defer) + continue; + + auto osVal = ruleObj.value("os"); + if (!osVal.isObject()) + { + // add a new implicit action rule + rules.append(ImplicitRule::create(action)); + continue; + } + + auto osObj = osVal.toObject(); + auto osNameVal = osObj.value("name"); + if (!osNameVal.isString()) + continue; + OpSys requiredOs = OpSys_fromString(osNameVal.toString()); + QString versionRegex = osObj.value("version").toString(); + // add a new OS rule + rules.append(OsRule::create(action, requiredOs, versionRegex)); + } + return rules; +} + +QJsonObject ImplicitRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + return ruleObj; +} + +QJsonObject OsRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + QJsonObject osObj; + { + osObj.insert("name", OpSys_toString(m_system)); + if(!m_version_regexp.isEmpty()) + { + osObj.insert("version", m_version_regexp); + } + } + ruleObj.insert("os", osObj); + return ruleObj; +} + diff --git a/api/logic/minecraft/Rule.h b/api/logic/minecraft/Rule.h new file mode 100644 index 00000000..c8bf6eaa --- /dev/null +++ b/api/logic/minecraft/Rule.h @@ -0,0 +1,101 @@ +/* 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 <QString> +#include <QList> +#include <QJsonObject> +#include <memory> +#include "OpSys.h" + +class Library; +class Rule; + +enum RuleAction +{ + Allow, + Disallow, + Defer +}; + +QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules); + +class Rule +{ +protected: + RuleAction m_result; + virtual bool applies(const Library *parent) = 0; + +public: + Rule(RuleAction result) : m_result(result) + { + } + virtual ~Rule() {}; + virtual QJsonObject toJson() = 0; + RuleAction apply(const Library *parent) + { + if (applies(parent)) + return m_result; + else + return Defer; + } +}; + +class OsRule : public Rule +{ +private: + // the OS + OpSys m_system; + // the OS version regexp + QString m_version_regexp; + +protected: + virtual bool applies(const Library *) + { + return (m_system == currentSystem); + } + OsRule(RuleAction result, OpSys system, QString version_regexp) + : Rule(result), m_system(system), m_version_regexp(version_regexp) + { + } + +public: + virtual QJsonObject toJson(); + static std::shared_ptr<OsRule> create(RuleAction result, OpSys system, + QString version_regexp) + { + return std::shared_ptr<OsRule>(new OsRule(result, system, version_regexp)); + } +}; + +class ImplicitRule : public Rule +{ +protected: + virtual bool applies(const Library *) + { + return true; + } + ImplicitRule(RuleAction result) : Rule(result) + { + } + +public: + virtual QJsonObject toJson(); + static std::shared_ptr<ImplicitRule> create(RuleAction result) + { + return std::shared_ptr<ImplicitRule>(new ImplicitRule(result)); + } +}; diff --git a/api/logic/minecraft/VersionBuildError.h b/api/logic/minecraft/VersionBuildError.h new file mode 100644 index 00000000..fda453e5 --- /dev/null +++ b/api/logic/minecraft/VersionBuildError.h @@ -0,0 +1,58 @@ +#include "Exception.h" + +class VersionBuildError : public Exception +{ +public: + explicit VersionBuildError(QString cause) : Exception(cause) {} + virtual ~VersionBuildError() noexcept + { + } +}; + +/** + * the base version file was meant for a newer version of the vanilla launcher than we support + */ +class LauncherVersionError : public VersionBuildError +{ +public: + LauncherVersionError(int actual, int supported) + : VersionBuildError(QObject::tr( + "The base version file of this instance was meant for a newer (%1) " + "version of the vanilla launcher than this version of MultiMC supports (%2).") + .arg(actual) + .arg(supported)) {}; + virtual ~LauncherVersionError() noexcept + { + } +}; + +/** + * some patch was intended for a different version of minecraft + */ +class MinecraftVersionMismatch : public VersionBuildError +{ +public: + MinecraftVersionMismatch(QString fileId, QString mcVersion, QString parentMcVersion) + : VersionBuildError(QObject::tr("The patch %1 is for a different version of Minecraft " + "(%2) than that of the instance (%3).") + .arg(fileId) + .arg(mcVersion) + .arg(parentMcVersion)) {}; + virtual ~MinecraftVersionMismatch() noexcept + { + } +}; + +/** + * files required for the version are not (yet?) present + */ +class VersionIncomplete : public VersionBuildError +{ +public: + VersionIncomplete(QString missingPatch) + : VersionBuildError(QObject::tr("Version is incomplete: missing %1.") + .arg(missingPatch)) {}; + virtual ~VersionIncomplete() noexcept + { + } +}; diff --git a/api/logic/minecraft/VersionFile.cpp b/api/logic/minecraft/VersionFile.cpp new file mode 100644 index 00000000..573c4cb4 --- /dev/null +++ b/api/logic/minecraft/VersionFile.cpp @@ -0,0 +1,60 @@ +#include <QJsonArray> +#include <QJsonDocument> + +#include <QDebug> + +#include "minecraft/VersionFile.h" +#include "minecraft/Library.h" +#include "minecraft/MinecraftProfile.h" +#include "minecraft/JarMod.h" +#include "ParseUtils.h" + +#include "VersionBuildError.h" +#include <Version.h> + +bool VersionFile::isMinecraftVersion() +{ + return fileId == "net.minecraft"; +} + +bool VersionFile::hasJarMods() +{ + return !jarMods.isEmpty(); +} + +void VersionFile::applyTo(MinecraftProfile *profile) +{ + auto theirVersion = profile->getMinecraftVersion(); + if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull()) + { + if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(theirVersion) == -1) + { + throw MinecraftVersionMismatch(fileId, dependsOnMinecraftVersion, theirVersion); + } + } + profile->applyMinecraftVersion(minecraftVersion); + profile->applyMainClass(mainClass); + profile->applyAppletClass(appletClass); + profile->applyMinecraftArguments(minecraftArguments); + if (isMinecraftVersion()) + { + profile->applyMinecraftVersionType(type); + } + profile->applyMinecraftAssets(mojangAssetIndex); + profile->applyTweakers(addTweakers); + + profile->applyJarMods(jarMods); + profile->applyTraits(traits); + + for (auto library : libraries) + { + profile->applyLibrary(library); + } + profile->applyProblemSeverity(getProblemSeverity()); + auto iter = mojangDownloads.begin(); + while(iter != mojangDownloads.end()) + { + profile->applyMojangDownload(iter.key(), iter.value()); + iter++; + } +} diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h new file mode 100644 index 00000000..1b692f0f --- /dev/null +++ b/api/logic/minecraft/VersionFile.h @@ -0,0 +1,195 @@ +#pragma once + +#include <QString> +#include <QStringList> +#include <QDateTime> +#include <QSet> + +#include <memory> +#include "minecraft/OpSys.h" +#include "minecraft/Rule.h" +#include "ProfilePatch.h" +#include "Library.h" +#include "JarMod.h" + +class MinecraftProfile; +class VersionFile; +struct MojangDownloadInfo; +struct MojangAssetIndexInfo; + +typedef std::shared_ptr<VersionFile> VersionFilePtr; +class VersionFile : public ProfilePatch +{ + friend class MojangVersionFormat; + friend class OneSixVersionFormat; +public: /* methods */ + virtual void applyTo(MinecraftProfile *profile) override; + virtual bool isMinecraftVersion() override; + virtual bool hasJarMods() override; + virtual int getOrder() override + { + return order; + } + virtual void setOrder(int order) override + { + this->order = order; + } + virtual QList<JarmodPtr> getJarMods() override + { + return jarMods; + } + virtual QString getID() override + { + return fileId; + } + virtual QString getName() override + { + return name; + } + virtual QString getVersion() override + { + return version; + } + virtual QString getFilename() override + { + return filename; + } + virtual QDateTime getReleaseDateTime() override + { + return m_releaseTime; + } + VersionSource getVersionSource() override + { + return Local; + } + + std::shared_ptr<class VersionFile> getVersionFile() override + { + return std::dynamic_pointer_cast<VersionFile>(shared_from_this()); + } + + virtual bool isCustom() override + { + return !m_isVanilla; + }; + virtual bool isCustomizable() override + { + return m_isCustomizable; + } + virtual bool isRemovable() override + { + return m_isRemovable; + } + virtual bool isRevertible() override + { + return m_isRevertible; + } + virtual bool isMoveable() override + { + return m_isMovable; + } + virtual bool isEditable() override + { + return isCustom(); + } + virtual bool isVersionChangeable() override + { + return false; + } + + void setVanilla (bool state) + { + m_isVanilla = state; + } + void setRemovable (bool state) + { + m_isRemovable = state; + } + void setRevertible (bool state) + { + m_isRevertible = state; + } + void setCustomizable (bool state) + { + m_isCustomizable = state; + } + void setMovable (bool state) + { + m_isMovable = state; + } + + +public: /* data */ + /// MultiMC: order hint for this version file if no explicit order is set + int order = 0; + + // Flags for UI and version file manipulation in general + bool m_isVanilla = false; + bool m_isRemovable = false; + bool m_isRevertible = false; + bool m_isCustomizable = false; + bool m_isMovable = false; + + /// MultiMC: filename of the file this was loaded from + QString filename; + + /// MultiMC: human readable name of this package + QString name; + + /// MultiMC: package ID of this package + QString fileId; + + /// MultiMC: version of this package + QString version; + + /// MultiMC: dependency on a Minecraft version + QString dependsOnMinecraftVersion; + + /// Mojang: used to version the Mojang version format + int minimumLauncherVersion = -1; + + /// Mojang: version of Minecraft this is + QString minecraftVersion; + + /// Mojang: class to launch Minecraft with + QString mainClass; + + /// MultiMC: DEPRECATED class to launch legacy Minecraft with (ambed in a custom window) + QString appletClass; + + /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) + QString minecraftArguments; + + /// Mojang: type of the Minecraft version + QString type; + + /// Mojang: the time this version was actually released by Mojang + QDateTime m_releaseTime; + + /// Mojang: the time this version was last updated by Mojang + QDateTime m_updateTime; + + /// Mojang: DEPRECATED asset group to be used with Minecraft + QString assets; + + /// MultiMC: list of tweaker mod arguments for launchwrapper + QStringList addTweakers; + + /// Mojang: list of libraries to add to the version + QList<LibraryPtr> libraries; + + /// MultiMC: list of attached traits of this version file - used to enable features + QSet<QString> traits; + + /// MultiMC: list of jar mods added to this version + QList<JarmodPtr> jarMods; + +public: + // Mojang: list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap <QString, std::shared_ptr<MojangDownloadInfo>> mojangDownloads; + + // Mojang: extended asset index download information + std::shared_ptr<MojangAssetIndexInfo> mojangAssetIndex; +}; + + diff --git a/api/logic/minecraft/VersionFilterData.cpp b/api/logic/minecraft/VersionFilterData.cpp new file mode 100644 index 00000000..0c4a6e3d --- /dev/null +++ b/api/logic/minecraft/VersionFilterData.cpp @@ -0,0 +1,75 @@ +#include "VersionFilterData.h" +#include "ParseUtils.h" + +VersionFilterData g_VersionFilterData = VersionFilterData(); + +VersionFilterData::VersionFilterData() +{ + // 1.3.* + auto libs13 = + QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}}; + + fmlLibsMapping["1.3.2"] = libs13; + + // 1.4.* + auto libs14 = QList<FMLlib>{ + {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}, + {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", false}}; + + fmlLibsMapping["1.4"] = libs14; + fmlLibsMapping["1.4.1"] = libs14; + fmlLibsMapping["1.4.2"] = libs14; + fmlLibsMapping["1.4.3"] = libs14; + fmlLibsMapping["1.4.4"] = libs14; + fmlLibsMapping["1.4.5"] = libs14; + fmlLibsMapping["1.4.6"] = libs14; + fmlLibsMapping["1.4.7"] = libs14; + + // 1.5 + fmlLibsMapping["1.5"] = QList<FMLlib>{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, + {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", false}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + + // 1.5.1 + fmlLibsMapping["1.5.1"] = QList<FMLlib>{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, + {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", false}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + + // 1.5.2 + fmlLibsMapping["1.5.2"] = QList<FMLlib>{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, + {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", false}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + + // don't use installers for those. + forgeInstallerBlacklist = QSet<QString>({"1.5.2"}); + // these won't show up in version lists because they are extremely bad and dangerous + legacyBlacklist = QSet<QString>({"rd-160052"}); + /* + * nothing older than this will be accepted from Mojang servers + * (these versions need to be tested by us first) + */ + legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); + lwjglWhitelist = + QSet<QString>{"net.java.jinput:jinput", "net.java.jinput:jinput-platform", + "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", + "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"}; + + // Version list magic + recommendedMinecraftVersion = "1.7.10"; +} diff --git a/api/logic/minecraft/VersionFilterData.h b/api/logic/minecraft/VersionFilterData.h new file mode 100644 index 00000000..f7d4ebe7 --- /dev/null +++ b/api/logic/minecraft/VersionFilterData.h @@ -0,0 +1,32 @@ +#pragma once +#include <QMap> +#include <QString> +#include <QSet> +#include <QDateTime> + +#include "multimc_logic_export.h" + +struct FMLlib +{ + QString filename; + QString checksum; + bool ours; +}; + +struct VersionFilterData +{ + VersionFilterData(); + // mapping between minecraft versions and FML libraries required + QMap<QString, QList<FMLlib>> fmlLibsMapping; + // set of minecraft versions for which using forge installers is blacklisted + QSet<QString> forgeInstallerBlacklist; + // set of 'legacy' versions that will not show up in the version lists. + QSet<QString> legacyBlacklist; + // no new versions below this date will be accepted from Mojang servers + QDateTime legacyCutoffDate; + // Libraries that belong to LWJGL + QSet<QString> lwjglWhitelist; + // Currently recommended minecraft version + QString recommendedMinecraftVersion; +}; +extern VersionFilterData MULTIMC_LOGIC_EXPORT g_VersionFilterData; diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp new file mode 100644 index 00000000..6081a8ec --- /dev/null +++ b/api/logic/minecraft/World.cpp @@ -0,0 +1,385 @@ +/* Copyright 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 <QDir> +#include <QString> +#include <QDebug> +#include <QSaveFile> +#include "World.h" + +#include "GZip.h" +#include <MMCZip.h> +#include <FileSystem.h> +#include <sstream> +#include <io/stream_reader.h> +#include <tag_string.h> +#include <tag_primitive.h> +#include <quazip.h> +#include <quazipfile.h> +#include <quazipdir.h> + +std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data) +{ + QByteArray output; + if(!GZip::unzip(data, output)) + { + return nullptr; + } + std::istringstream foo(std::string(output.constData(), output.size())); + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); +} + +QByteArray serializeLevelDat(nbt::tag_compound * levelInfo) +{ + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val( s.str().data(), (int) s.str().size() ); + return val; +} + +QString getLevelDatFromFS(const QFileInfo &file) +{ + QDir worldDir(file.filePath()); + if(!file.isDir() || !worldDir.exists("level.dat")) + { + return QString(); + } + return worldDir.absoluteFilePath("level.dat"); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo &file) +{ + auto fullFilePath = getLevelDatFromFS(file); + if(fullFilePath.isNull()) + { + return QByteArray(); + } + QFile f(fullFilePath); + if(!f.open(QIODevice::ReadOnly)) + { + return QByteArray(); + } + return f.readAll(); +} + +bool putLevelDatDataToFS(const QFileInfo &file, QByteArray & data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if(fullFilePath.isNull()) + { + return false; + } + QSaveFile f(fullFilePath); + if(!f.open(QIODevice::WriteOnly)) + { + return false; + } + QByteArray compressed; + if(!GZip::zip(data, compressed)) + { + return false; + } + if(f.write(compressed) != compressed.size()) + { + f.cancelWriting(); + return false; + } + return f.commit(); +} + +World::World(const QFileInfo &file) +{ + repath(file); +} + +void World::repath(const QFileInfo &file) +{ + m_containerFile = file; + m_folderName = file.fileName(); + if(file.isFile() && file.suffix() == "zip") + { + readFromZip(file); + } + else if(file.isDir()) + { + readFromFS(file); + } +} + +void World::readFromFS(const QFileInfo &file) +{ + auto bytes = getLevelDatDataFromFS(file); + if(bytes.isEmpty()) + { + is_valid = false; + return; + } + loadFromLevelDat(bytes); + levelDatTime = file.lastModified(); +} + +void World::readFromZip(const QFileInfo &file) +{ + QuaZip zip(file.absoluteFilePath()); + is_valid = zip.open(QuaZip::mdUnzip); + if (!is_valid) + { + return; + } + auto location = MMCZip::findFileInZip(&zip, "level.dat"); + is_valid = !location.isEmpty(); + if (!is_valid) + { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + is_valid = zip.setCurrentFile(location + "level.dat"); + if (!is_valid) + { + return; + } + is_valid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if(!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + levelDatTime = modTime; + if (!is_valid) + { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(const QString &to, const QString &name) +{ + auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); + if(!FS::ensureFolderPathExists(finalPath)) + { + return false; + } + bool ok = false; + if(m_containerFile.isFile()) + { + QuaZip zip(m_containerFile.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty(); + } + else if(m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if(ok && !name.isEmpty() && m_actualName != name) + { + World newWorld(finalPath); + if(newWorld.isValid()) + { + newWorld.rename(name); + } + } + return ok; +} + +bool World::rename(const QString &newName) +{ + if(m_containerFile.isFile()) + { + return false; + } + + auto data = getLevelDatDataFromFS(m_containerFile); + if(data.isEmpty()) + { + return false; + } + + auto worldData = parseLevelDat(data); + if(!worldData) + { + return false; + } + auto &val = worldData->at("Data"); + if(val.get_type() != nbt::tag_type::Compound) + { + return false; + } + auto &dataCompound = val.as<nbt::tag_compound>(); + dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); + data = serializeLevelDat(worldData.get()); + + putLevelDatDataToFS(m_containerFile, data); + + m_actualName = newName; + + QDir parentDir(m_containerFile.absoluteFilePath()); + parentDir.cdUp(); + QFile container(m_containerFile.absoluteFilePath()); + auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); + container.rename(parentDir.absoluteFilePath(dirName)); + + return true; +} + +static QString read_string (nbt::value& parent, const char * name, const QString & fallback = QString()) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::String) + { + return fallback; + } + auto & tag_str = namedValue.as<nbt::tag_string>(); + return QString::fromStdString(tag_str.get()); + } + catch(std::out_of_range e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback; + return fallback; + } + catch(std::bad_cast e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback; + return fallback; + } +}; + +static int64_t read_long (nbt::value& parent, const char * name, const int64_t & fallback = 0) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Long) + { + return fallback; + } + auto & tag_str = namedValue.as<nbt::tag_long>(); + return tag_str.get(); + } + catch(std::out_of_range e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback; + return fallback; + } + catch(std::bad_cast e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback; + return fallback; + } +}; + +void World::loadFromLevelDat(QByteArray data) +{ + try + { + auto levelData = parseLevelDat(data); + if(!levelData) + { + is_valid = false; + return; + } + + auto &val = levelData->at("Data"); + is_valid = val.get_type() == nbt::tag_type::Compound; + if(!is_valid) + return; + + m_actualName = read_string(val, "LevelName", m_folderName); + + + int64_t temp = read_long(val, "LastPlayed", 0); + if(temp == 0) + { + m_lastPlayed = levelDatTime; + } + else + { + m_lastPlayed = QDateTime::fromMSecsSinceEpoch(temp); + } + + m_randomSeed = read_long(val, "RandomSeed", 0); + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + qDebug() << "Seed:" << m_randomSeed; + } + catch (nbt::io::input_error e) + { + qWarning() << "Unable to load" << m_folderName << ":" << e.what(); + is_valid = false; + return; + } +} + +bool World::replace(World &with) +{ + if (!destroy()) + return false; + bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); + if (success) + { + m_folderName = with.m_folderName; + m_containerFile.refresh(); + } + return success; +} + +bool World::destroy() +{ + if(!is_valid) return false; + if (m_containerFile.isDir()) + { + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); + } + else if(m_containerFile.isFile()) + { + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); + } + return true; +} + +bool World::operator==(const World &other) const +{ + return is_valid == other.is_valid && folderName() == other.folderName(); +} +bool World::strongCompare(const World &other) const +{ + return is_valid == other.is_valid && folderName() == other.folderName(); +} diff --git a/api/logic/minecraft/World.h b/api/logic/minecraft/World.h new file mode 100644 index 00000000..3cde5ea4 --- /dev/null +++ b/api/logic/minecraft/World.h @@ -0,0 +1,83 @@ +/* Copyright 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 <QFileInfo> +#include <QDateTime> + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT World +{ +public: + World(const QFileInfo &file); + QString folderName() const + { + return m_folderName; + } + QString name() const + { + return m_actualName; + } + QDateTime lastPlayed() const + { + return m_lastPlayed; + } + int64_t seed() const + { + return m_randomSeed; + } + bool isValid() const + { + return is_valid; + } + bool isOnFS() const + { + return m_containerFile.isDir(); + } + QFileInfo container() const + { + return m_containerFile; + } + // delete all the files of this world + bool destroy(); + // replace this world with a copy of the other + bool replace(World &with); + // change the world's filesystem path (used by world lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + bool rename(const QString &to); + bool install(const QString &to, const QString &name= QString()); + + // WEAK compare operator - used for replacing worlds + bool operator==(const World &other) const; + bool strongCompare(const World &other) const; + +private: + void readFromZip(const QFileInfo &file); + void readFromFS(const QFileInfo &file); + void loadFromLevelDat(QByteArray data); + +protected: + + QFileInfo m_containerFile; + QString m_containerOffsetPath; + QString m_folderName; + QString m_actualName; + QDateTime levelDatTime; + QDateTime m_lastPlayed; + int64_t m_randomSeed = 0; + bool is_valid = false; +}; diff --git a/api/logic/minecraft/WorldList.cpp b/api/logic/minecraft/WorldList.cpp new file mode 100644 index 00000000..42c8a3e6 --- /dev/null +++ b/api/logic/minecraft/WorldList.cpp @@ -0,0 +1,355 @@ +/* Copyright 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 "WorldList.h" +#include <FileSystem.h> +#include <QMimeData> +#include <QUrl> +#include <QUuid> +#include <QString> +#include <QFileSystemWatcher> +#include <QDebug> + +WorldList::WorldList(const QString &dir) + : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + is_watching = false; + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void WorldList::startWatching() +{ + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void WorldList::stopWatching() +{ + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool WorldList::update() +{ + if (!isValid()) + return false; + + QList<World> newWorlds; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if(!entry.isDir()) + continue; + + World w(entry); + if(w.isValid()) + { + newWorlds.append(w); + } + } + beginResetModel(); + worlds.swap(newWorlds); + endResetModel(); + return true; +} + +void WorldList::directoryChanged(QString path) +{ + update(); +} + +bool WorldList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool WorldList::deleteWorld(int index) +{ + if (index >= worlds.size() || index < 0) + return false; + World &m = worlds[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + worlds.removeAt(index); + endRemoveRows(); + emit changed(); + return true; + } + return false; +} + +bool WorldList::deleteWorlds(int first, int last) +{ + for (int i = first; i <= last; i++) + { + World &m = worlds[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); + endRemoveRows(); + emit changed(); + return true; +} + +int WorldList::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +QVariant WorldList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= worlds.size()) + return QVariant(); + + auto & world = worlds[row]; + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return world.name(); + + case LastPlayedColumn: + return world.lastPlayed(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + { + return world.folderName(); + } + case ObjectRole: + { + return QVariant::fromValue<void *>((void *)&world); + } + case FolderRole: + { + return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); + } + case SeedRole: + { + return qVariantFromValue<qlonglong>(world.seed()); + } + case NameRole: + { + return world.name(); + } + case LastPlayedRole: + { + return world.lastPlayed(); + } + default: + return QVariant(); + } +} + +QVariant WorldList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: + return tr("Name"); + case LastPlayedColumn: + return tr("Last Played"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("The name of the world."); + case LastPlayedColumn: + return tr("Date and time the world was last played."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +QStringList WorldList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +class WorldMimeData : public QMimeData +{ +Q_OBJECT + +public: + WorldMimeData(QList<World> worlds) + { + m_worlds = worlds; + + } + QStringList formats() const + { + return QMimeData::formats() << "text/uri-list"; + } + +protected: + QVariant retrieveData(const QString &mimetype, QVariant::Type type) const + { + QList<QUrl> urls; + for(auto &world: m_worlds) + { + if(!world.isValid() || !world.isOnFS()) + continue; + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); + } + const_cast<WorldMimeData*>(this)->setUrls(urls); + return QMimeData::retrieveData(mimetype, type); + } +private: + QList<World> m_worlds; +}; + +QMimeData *WorldList::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.size() == 0) + return new QMimeData(); + + QList<World> worlds; + for(auto idx : indexes) + { + if(idx.column() != 0) + continue; + int row = idx.row(); + if (row < 0 || row >= this->worlds.size()) + continue; + worlds.append(this->worlds[row]); + } + if(!worlds.size()) + { + return new QMimeData(); + } + return new WorldMimeData(worlds); +} + +Qt::ItemFlags WorldList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions WorldList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +Qt::DropActions WorldList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +void WorldList::installWorld(QFileInfo filename) +{ + qDebug() << "installing: " << filename.absoluteFilePath(); + World w(filename); + if(!w.isValid()) + { + return; + } + w.install(m_dir.absolutePath()); +} + +bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = is_watching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + + QFileInfo worldInfo(filename); + + if(!m_dir.entryInfoList().contains(worldInfo)) + { + installWorld(worldInfo); + } + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +#include "WorldList.moc" diff --git a/api/logic/minecraft/WorldList.h b/api/logic/minecraft/WorldList.h new file mode 100644 index 00000000..34b30e9c --- /dev/null +++ b/api/logic/minecraft/WorldList.h @@ -0,0 +1,125 @@ +/* Copyright 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 <QList> +#include <QString> +#include <QDir> +#include <QAbstractListModel> +#include <QMimeData> +#include "minecraft/World.h" + +#include "multimc_logic_export.h" + +class QFileSystemWatcher; + +class MULTIMC_LOGIC_EXPORT WorldList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + NameColumn, + LastPlayedColumn + }; + + enum Roles + { + ObjectRole = Qt::UserRole + 1, + FolderRole, + SeedRole, + NameRole, + LastPlayedRole + }; + + WorldList(const QString &dir); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return size(); + }; + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex &parent) const; + + size_t size() const + { + return worlds.size(); + }; + bool empty() const + { + return size() == 0; + } + World &operator[](size_t index) + { + return worlds[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(QFileInfo filename); + + /// Deletes the mod at the given index. + virtual bool deleteWorld(int index); + + /// Deletes all the selected mods + virtual bool deleteWorlds(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + /// get data for drag action + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() const + { + return m_dir; + } + + const QList<World> &allWorlds() const + { + return worlds; + } + +private slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching; + QDir m_dir; + QList<World> worlds; +}; diff --git a/api/logic/minecraft/auth/AuthSession.cpp b/api/logic/minecraft/auth/AuthSession.cpp new file mode 100644 index 00000000..8758bfbd --- /dev/null +++ b/api/logic/minecraft/auth/AuthSession.cpp @@ -0,0 +1,30 @@ +#include "AuthSession.h" +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonDocument> +#include <QStringList> + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); + +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + if (status != PlayableOffline && status != PlayableOnline) + { + return false; + } + session = "-"; + player_name = offline_playername; + status = PlayableOffline; + return true; +} diff --git a/api/logic/minecraft/auth/AuthSession.h b/api/logic/minecraft/auth/AuthSession.h new file mode 100644 index 00000000..dede90a9 --- /dev/null +++ b/api/logic/minecraft/auth/AuthSession.h @@ -0,0 +1,51 @@ +#pragma once + +#include <QString> +#include <QMultiMap> +#include <memory> + +#include "multimc_logic_export.h" + +struct User +{ + QString id; + QMultiMap<QString, QString> properties; +}; + +struct MULTIMC_LOGIC_EXPORT AuthSession +{ + bool MakeOffline(QString offline_playername); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresPassword, + PlayableOffline, + PlayableOnline + } status = Undetermined; + + User u; + + // client token + QString client_token; + // account user name + QString username; + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; +}; + +typedef std::shared_ptr<AuthSession> AuthSessionPtr; diff --git a/api/logic/minecraft/auth/MojangAccount.cpp b/api/logic/minecraft/auth/MojangAccount.cpp new file mode 100644 index 00000000..69a24c09 --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccount.cpp @@ -0,0 +1,278 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan <orochimarufan.x3@gmail.com> + * + * 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 "MojangAccount.h" +#include "flows/RefreshTask.h" +#include "flows/AuthenticateTask.h" + +#include <QUuid> +#include <QJsonObject> +#include <QJsonArray> +#include <QRegExp> +#include <QStringList> +#include <QJsonDocument> + +#include <QDebug> + +MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) +{ + // The JSON object must at least have a username for it to be valid. + if (!object.value("username").isString()) + { + qCritical() << "Can't load Mojang account info from JSON object. Username field is " + "missing or of the wrong type."; + return nullptr; + } + + QString username = object.value("username").toString(""); + QString clientToken = object.value("clientToken").toString(""); + QString accessToken = object.value("accessToken").toString(""); + + QJsonArray profileArray = object.value("profiles").toArray(); + if (profileArray.size() < 1) + { + qCritical() << "Can't load Mojang account with username \"" << username + << "\". No profiles found."; + return nullptr; + } + + QList<AccountProfile> profiles; + for (QJsonValue profileVal : profileArray) + { + QJsonObject profileObject = profileVal.toObject(); + QString id = profileObject.value("id").toString(""); + QString name = profileObject.value("name").toString(""); + bool legacy = profileObject.value("legacy").toBool(false); + if (id.isEmpty() || name.isEmpty()) + { + qWarning() << "Unable to load a profile because it was missing an ID or a name."; + continue; + } + profiles.append({id, name, legacy}); + } + + MojangAccountPtr account(new MojangAccount()); + if (object.value("user").isObject()) + { + User u; + QJsonObject userStructure = object.value("user").toObject(); + u.id = userStructure.value("id").toString(); + /* + QJsonObject propMap = userStructure.value("properties").toObject(); + for(auto key: propMap.keys()) + { + auto values = propMap.operator[](key).toArray(); + for(auto value: values) + u.properties.insert(key, value.toString()); + } + */ + account->m_user = u; + } + account->m_username = username; + account->m_clientToken = clientToken; + account->m_accessToken = accessToken; + account->m_profiles = profiles; + + // Get the currently selected profile. + QString currentProfile = object.value("activeProfile").toString(""); + if (!currentProfile.isEmpty()) + account->setCurrentProfile(currentProfile); + + return account; +} + +MojangAccountPtr MojangAccount::createFromUsername(const QString &username) +{ + MojangAccountPtr account(new MojangAccount()); + account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->m_username = username; + return account; +} + +QJsonObject MojangAccount::saveToJson() const +{ + QJsonObject json; + json.insert("username", m_username); + json.insert("clientToken", m_clientToken); + json.insert("accessToken", m_accessToken); + + QJsonArray profileArray; + for (AccountProfile profile : m_profiles) + { + QJsonObject profileObj; + profileObj.insert("id", profile.id); + profileObj.insert("name", profile.name); + profileObj.insert("legacy", profile.legacy); + profileArray.append(profileObj); + } + json.insert("profiles", profileArray); + + QJsonObject userStructure; + { + userStructure.insert("id", m_user.id); + /* + QJsonObject userAttrs; + for(auto key: m_user.properties.keys()) + { + auto array = QJsonArray::fromStringList(m_user.properties.values(key)); + userAttrs.insert(key, array); + } + userStructure.insert("properties", userAttrs); + */ + } + json.insert("user", userStructure); + + if (m_currentProfile != -1) + json.insert("activeProfile", currentProfile()->id); + + return json; +} + +bool MojangAccount::setCurrentProfile(const QString &profileId) +{ + for (int i = 0; i < m_profiles.length(); i++) + { + if (m_profiles[i].id == profileId) + { + m_currentProfile = i; + return true; + } + } + return false; +} + +const AccountProfile *MojangAccount::currentProfile() const +{ + if (m_currentProfile == -1) + return nullptr; + return &m_profiles[m_currentProfile]; +} + +AccountStatus MojangAccount::accountStatus() const +{ + if (m_accessToken.isEmpty()) + return NotVerified; + else + return Verified; +} + +std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session, + QString password) +{ + Q_ASSERT(m_currentTask.get() == nullptr); + + // take care of the true offline status + if (accountStatus() == NotVerified && password.isEmpty()) + { + if (session) + { + session->status = AuthSession::RequiresPassword; + fillSession(session); + } + return nullptr; + } + + if (password.isEmpty()) + { + m_currentTask.reset(new RefreshTask(this)); + } + else + { + m_currentTask.reset(new AuthenticateTask(this, password)); + } + m_currentTask->assignSession(session); + + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + return m_currentTask; +} + +void MojangAccount::authSucceeded() +{ + auto session = m_currentTask->getAssignedSession(); + if (session) + { + session->status = + session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline; + fillSession(session); + session->auth_server_online = true; + } + m_currentTask.reset(); + emit changed(); +} + +void MojangAccount::authFailed(QString reason) +{ + auto session = m_currentTask->getAssignedSession(); + // This is emitted when the yggdrasil tasks time out or are cancelled. + // -> we treat the error as no-op + if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT) + { + if (session) + { + session->status = accountStatus() == Verified ? AuthSession::PlayableOffline + : AuthSession::RequiresPassword; + session->auth_server_online = false; + fillSession(session); + } + } + else + { + m_accessToken = QString(); + emit changed(); + if (session) + { + session->status = AuthSession::RequiresPassword; + session->auth_server_online = true; + fillSession(session); + } + } + m_currentTask.reset(); +} + +void MojangAccount::fillSession(AuthSessionPtr session) +{ + // the user name. you have to have an user name + session->username = m_username; + // volatile auth token + session->access_token = m_accessToken; + // the semi-permanent client token + session->client_token = m_clientToken; + if (currentProfile()) + { + // profile name + session->player_name = currentProfile()->name; + // profile ID + session->uuid = currentProfile()->id; + // 'legacy' or 'mojang', depending on account type + session->user_type = currentProfile()->legacy ? "legacy" : "mojang"; + if (!session->access_token.isEmpty()) + { + session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; + } + else + { + session->session = "-"; + } + } + else + { + session->player_name = "Player"; + session->session = "-"; + } + session->u = user(); +} diff --git a/api/logic/minecraft/auth/MojangAccount.h b/api/logic/minecraft/auth/MojangAccount.h new file mode 100644 index 00000000..2de0c19c --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccount.h @@ -0,0 +1,173 @@ +/* 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 <QObject> +#include <QString> +#include <QList> +#include <QJsonObject> +#include <QPair> +#include <QMap> + +#include <memory> +#include "AuthSession.h" + +#include "multimc_logic_export.h" + +class Task; +class YggdrasilTask; +class MojangAccount; + +typedef std::shared_ptr<MojangAccount> MojangAccountPtr; +Q_DECLARE_METATYPE(MojangAccountPtr) + +/** + * A profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in MultiMC right now so + * we don't have to rip the code to pieces to add it later. + */ +struct AccountProfile +{ + QString id; + QString name; + bool legacy; +}; + +enum AccountStatus +{ + NotVerified, + Verified +}; + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MULTIMC_LOGIC_EXPORT MojangAccount : public QObject +{ + Q_OBJECT +public: /* construction */ + //! Do not copy accounts. ever. + explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; + + //! Default constructor + explicit MojangAccount(QObject *parent = 0) : QObject(parent) {}; + + //! Creates an empty account for the specified user name. + static MojangAccountPtr createFromUsername(const QString &username); + + //! Loads a MojangAccount from the given JSON object. + static MojangAccountPtr loadFromJson(const QJsonObject &json); + + //! Saves a MojangAccount to a JSON object and returns it. + QJsonObject saveToJson() const; + +public: /* manipulation */ + /** + * Sets the currently selected profile to the profile with the given ID string. + * If profileId is not in the list of available profiles, the function will simply return + * false. + */ + bool setCurrentProfile(const QString &profileId); + + /** + * Attempt to login. Empty password means we use the token. + * If the attempt fails because we already are performing some task, it returns false. + */ + std::shared_ptr<YggdrasilTask> login(AuthSessionPtr session, + QString password = QString()); + +public: /* queries */ + const QString &username() const + { + return m_username; + } + + const QString &clientToken() const + { + return m_clientToken; + } + + const QString &accessToken() const + { + return m_accessToken; + } + + const QList<AccountProfile> &profiles() const + { + return m_profiles; + } + + const User &user() + { + return m_user; + } + + //! Returns the currently selected profile (if none, returns nullptr) + const AccountProfile *currentProfile() const; + + //! Returns whether the account is NotVerified, Verified or Online + AccountStatus accountStatus() const; + +signals: + /** + * This signal is emitted when the account changes + */ + void changed(); + + // TODO: better signalling for the various possible state changes - especially errors + +protected: /* variables */ + QString m_username; + + // Used to identify the client - the user can have multiple clients for the same account + // Think: different launchers, all connecting to the same account/profile + QString m_clientToken; + + // Blank if not logged in. + QString m_accessToken; + + // Index of the selected profile within the list of available + // profiles. -1 if nothing is selected. + int m_currentProfile = -1; + + // List of available profiles. + QList<AccountProfile> m_profiles; + + // the user structure, whatever it is. + User m_user; + + // current task we are executing here + std::shared_ptr<YggdrasilTask> m_currentTask; + +private +slots: + void authSucceeded(); + void authFailed(QString reason); + +private: + void fillSession(AuthSessionPtr session); + +public: + friend class YggdrasilTask; + friend class AuthenticateTask; + friend class ValidateTask; + friend class RefreshTask; +}; diff --git a/api/logic/minecraft/auth/MojangAccountList.cpp b/api/logic/minecraft/auth/MojangAccountList.cpp new file mode 100644 index 00000000..26cbc81a --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccountList.cpp @@ -0,0 +1,427 @@ +/* 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 "MojangAccountList.h" +#include "MojangAccount.h" + +#include <QIODevice> +#include <QFile> +#include <QTextStream> +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonParseError> +#include <QDir> + +#include <QDebug> + +#include <FileSystem.h> + +#define ACCOUNT_LIST_FORMAT_VERSION 2 + +MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) +{ +} + +MojangAccountPtr MojangAccountList::findAccount(const QString &username) const +{ + for (int i = 0; i < count(); i++) + { + MojangAccountPtr account = at(i); + if (account->username() == username) + return account; + } + return nullptr; +} + +const MojangAccountPtr MojangAccountList::at(int i) const +{ + return MojangAccountPtr(m_accounts.at(i)); +} + +void MojangAccountList::addAccount(const MojangAccountPtr account) +{ + beginResetModel(); + connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + m_accounts.append(account); + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(const QString &username) +{ + beginResetModel(); + for (auto account : m_accounts) + { + if (account->username() == username) + { + m_accounts.removeOne(account); + return; + } + } + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(QModelIndex index) +{ + beginResetModel(); + m_accounts.removeAt(index.row()); + endResetModel(); + onListChanged(); +} + +MojangAccountPtr MojangAccountList::activeAccount() const +{ + return m_activeAccount; +} + +void MojangAccountList::setActiveAccount(const QString &username) +{ + beginResetModel(); + if (username.isEmpty()) + { + m_activeAccount = nullptr; + } + else + { + for (MojangAccountPtr account : m_accounts) + { + if (account->username() == username) + m_activeAccount = account; + } + } + endResetModel(); + onActiveChanged(); +} + +void MojangAccountList::accountChanged() +{ + // the list changed. there is no doubt. + onListChanged(); +} + +void MojangAccountList::onListChanged() +{ + if (m_autosave) + // TODO: Alert the user if this fails. + saveList(); + + emit listChanged(); +} + +void MojangAccountList::onActiveChanged() +{ + if (m_autosave) + saveList(); + + emit activeAccountChanged(); +} + +int MojangAccountList::count() const +{ + return m_accounts.count(); +} + +QVariant MojangAccountList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MojangAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return account->username(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return account->username(); + + case PointerRole: + return qVariantFromValue(account); + + case Qt::CheckStateRole: + switch (index.column()) + { + case ActiveColumn: + return account == m_activeAccount; + } + + default: + return QVariant(); + } +} + +QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return tr("Active?"); + + case NameColumn: + return tr("Name"); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("The name of the version."); + + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +int MojangAccountList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int MojangAccountList::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if(role == Qt::CheckStateRole) + { + if(value == Qt::Checked) + { + MojangAccountPtr account = this->at(index.row()); + this->setActiveAccount(account->username()); + } + } + + emit dataChanged(index, index); + return true; +} + +void MojangAccountList::updateListData(QList<MojangAccountPtr> versions) +{ + beginResetModel(); + m_accounts = versions; + endResetModel(); +} + +bool MojangAccountList::loadList(const QString &filePath) +{ + QString path = filePath; + if (path.isEmpty()) + path = m_listFilePath; + if (path.isEmpty()) + { + qCritical() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION) + { + QString newName = "accounts-old.json"; + qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to" + << newName; + + // Attempt to rename the old version. + file.rename(newName); + return false; + } + + // Now, load the accounts array. + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MojangAccountPtr account = MojangAccount::loadFromJson(accountObj); + if (account.get() != nullptr) + { + connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + m_accounts.append(account); + } + else + { + qWarning() << "Failed to load an account."; + } + } + // Load the active account. + m_activeAccount = findAccount(root.value("activeAccount").toString("")); + endResetModel(); + return true; +} + +bool MojangAccountList::saveList(const QString &filePath) +{ + QString path(filePath); + if (path.isEmpty()) + path = m_listFilePath; + if (path.isEmpty()) + { + qCritical() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + // make sure the parent folder exists + if(!FS::ensureFilePathExists(path)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(path); + if(finfo.isDir()) + { + QDir badDir(path); + badDir.removeRecursively(); + } + + qDebug() << "Writing account list to" << path; + + qDebug() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION); + + // Build a list of accounts. + qDebug() << "Building account array."; + QJsonArray accounts; + for (MojangAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + if(m_activeAccount) + { + // Save the active account. + root.insert("activeAccount", m_activeAccount->username()); + } + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + // Now that we're done building the JSON object, we can write it to the file. + qDebug() << "Writing account list to file."; + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser); + file.close(); + + qDebug() << "Saved account list to" << path; + + return true; +} + +void MojangAccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + m_autosave = autosave; +} + +bool MojangAccountList::anyAccountIsValid() +{ + for(auto account:m_accounts) + { + if(account->accountStatus() != NotVerified) + return true; + } + return false; +} diff --git a/api/logic/minecraft/auth/MojangAccountList.h b/api/logic/minecraft/auth/MojangAccountList.h new file mode 100644 index 00000000..c40fa6a3 --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccountList.h @@ -0,0 +1,201 @@ +/* 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 "MojangAccount.h" + +#include <QObject> +#include <QVariant> +#include <QAbstractListModel> +#include <QSharedPointer> + +#include "multimc_logic_export.h" + +/*! + * \brief List of available Mojang accounts. + * This should be loaded in the background by MultiMC on startup. + * + * This class also inherits from QAbstractListModel. Methods from that + * class determine how this list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by subclasses to + * change the behavior of the list. + */ +class MULTIMC_LOGIC_EXPORT MojangAccountList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // TODO: Add icon column. + + // First column - Active? + ActiveColumn = 0, + + // Second column - Name + NameColumn, + }; + + explicit MojangAccountList(QObject *parent = 0); + + //! Gets the account at the given index. + virtual const MojangAccountPtr at(int i) const; + + //! Returns the number of accounts in the list. + virtual int count() const; + + //////// 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; + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role); + + /*! + * Adds a the given Mojang account to the account list. + */ + virtual void addAccount(const MojangAccountPtr account); + + /*! + * Removes the mojang account with the given username from the account list. + */ + virtual void removeAccount(const QString &username); + + /*! + * Removes the account at the given QModelIndex. + */ + virtual void removeAccount(QModelIndex index); + + /*! + * \brief Finds an account by its username. + * \param The username of the account to find. + * \return A const pointer to the account with the given username. NULL if + * one doesn't exist. + */ + virtual MojangAccountPtr findAccount(const QString &username) const; + + /*! + * Sets the default path to save the list file to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + virtual void setListFilePath(QString path, bool autosave = false); + + /*! + * \brief Loads the account list from the given file path. + * If the given file is an empty string (default), will load from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool loadList(const QString &file = ""); + + /*! + * \brief Saves the account list to the given file. + * If the given file is an empty string (default), will save from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool saveList(const QString &file = ""); + + /*! + * \brief Gets a pointer to the account that the user has selected as their "active" account. + * Which account is active can be overridden on a per-instance basis, but this will return the one that + * is set as active globally. + * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer. + */ + virtual MojangAccountPtr activeAccount() const; + + /*! + * Sets the given account as the current active account. + * If the username given is an empty string, sets the active account to nothing. + */ + virtual void setActiveAccount(const QString &username); + + /*! + * Returns true if any of the account is at least Validated + */ + bool anyAccountIsValid(); + +signals: + /*! + * Signal emitted to indicate that the account list has changed. + * This will also fire if the value of an element in the list changes (will be implemented + * later). + */ + void listChanged(); + + /*! + * Signal emitted to indicate that the active account has changed. + */ + void activeAccountChanged(); + +public +slots: + /** + * This is called when one of the accounts changes and the list needs to be updated + */ + void accountChanged(); + +protected: + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + /*! + * Called whenever the active account changes. + * Emits the activeAccountChanged() signal and autosaves the list if enabled. + */ + void onActiveChanged(); + + QList<MojangAccountPtr> m_accounts; + + /*! + * Account that is currently active. + */ + MojangAccountPtr m_activeAccount; + + //! Path to the account list file. Empty string if there isn't one. + QString m_listFilePath; + + /*! + * If true, the account list will automatically save to the account list path when it changes. + * Ignored if m_listFilePath is blank. + */ + bool m_autosave = false; + +protected +slots: + /*! + * Updates this list with the given list of accounts. + * This is done by copying each account in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the accounts are set to this + * account list. This can't be done in the load task, because the accounts 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 accounts and sets their parents correctly. + * \param accounts List of accounts whose parents should be set. + */ + virtual void updateListData(QList<MojangAccountPtr> versions); +}; diff --git a/api/logic/minecraft/auth/YggdrasilTask.cpp b/api/logic/minecraft/auth/YggdrasilTask.cpp new file mode 100644 index 00000000..c6971c9f --- /dev/null +++ b/api/logic/minecraft/auth/YggdrasilTask.cpp @@ -0,0 +1,255 @@ +/* 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 "YggdrasilTask.h" +#include "MojangAccount.h" + +#include <QObject> +#include <QString> +#include <QJsonObject> +#include <QJsonDocument> +#include <QNetworkReply> +#include <QByteArray> + +#include <Env.h> + +#include <net/URLConstants.h> + +#include <QDebug> + +YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent) + : Task(parent), m_account(account) +{ + changeState(STATE_CREATED); +} + +void YggdrasilTask::executeTask() +{ + changeState(STATE_SENDING_REQUEST); + + // Get the content of the request we're going to send to the server. + QJsonDocument doc(getRequestContent()); + + auto worker = ENV.qnam(); + QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint()); + QNetworkRequest netRequest(reqUrl); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QByteArray requestData = doc.toJson(); + m_netReply = worker->post(netRequest, requestData); + connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors); + timeout_keeper.setSingleShot(true); + timeout_keeper.start(timeout_max); + counter.setSingleShot(false); + counter.start(time_step); + progress(0, timeout_max); + connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout); + connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); +} + +void YggdrasilTask::refreshTimers(qint64, qint64) +{ + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} +void YggdrasilTask::heartbeat() +{ + count += time_step; + progress(count, timeout_max); +} + +bool YggdrasilTask::abort() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = YggdrasilTask::BY_USER; + m_netReply->abort(); + return true; +} + +void YggdrasilTask::abortByTimeout() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = YggdrasilTask::BY_TIMEOUT; + m_netReply->abort(); +} + +void YggdrasilTask::sslErrors(QList<QSslError> errors) +{ + int i = 1; + for (auto error : errors) + { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +void YggdrasilTask::processReply() +{ + changeState(STATE_PROCESSING_RESPONSE); + + switch (m_netReply->error()) + { + case QNetworkReply::NoError: + break; + case QNetworkReply::TimeoutError: + changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out.")); + return; + case QNetworkReply::OperationCanceledError: + changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); + return; + case QNetworkReply::SslHandshakeFailedError: + changeState( + STATE_FAILED_SOFT, + tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>" + "<ul>" + "<li>You use Windows XP and need to <a " + "href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update " + "your root certificates</a></li>" + "<li>Some device on your network is interfering with SSL traffic. In that case, " + "you have bigger worries than Minecraft not starting.</li>" + "<li>Possibly something else. Check the MultiMC log file for details</li>" + "</ul>")); + return; + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::ContentOperationNotPermittedError: + break; + default: + changeState(STATE_FAILED_SOFT, + tr("Authentication operation failed due to a network error: %1 (%2)") + .arg(m_netReply->errorString()).arg(m_netReply->error())); + return; + } + + // Try to parse the response regardless of the response code. + // Sometimes the auth server will give more information and an error code. + QJsonParseError jsonError; + QByteArray replyData = m_netReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + // Check the response code. + int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) + { + // If the response code was 200, then there shouldn't be an error. Make sure + // anyways. + // Also, sometimes an empty reply indicates success. If there was no data received, + // pass an empty json object to the processResponse function. + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) + { + processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); + return; + } + else + { + changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response " + "JSON response: %1 at offset %2.") + .arg(jsonError.errorString()) + .arg(jsonError.offset)); + qCritical() << replyData; + } + return; + } + + // If the response code was not 200, then Yggdrasil may have given us information + // about the error. + // If we can parse the response, then get information from it. Otherwise just say + // there was an unknown error. + if (jsonError.error == QJsonParseError::NoError) + { + // We were able to parse the server's response. Woo! + // Call processError. If a subclass has overridden it then they'll handle their + // stuff there. + qDebug() << "The request failed, but the server gave us an error message. " + "Processing error."; + processError(doc.object()); + } + else + { + // The server didn't say anything regarding the error. Give the user an unknown + // error. + qDebug() + << "The request failed and the server gave no error message. Unknown error."; + changeState(STATE_FAILED_SOFT, + tr("An unknown error occurred when trying to communicate with the " + "authentication server: %1").arg(m_netReply->errorString())); + } +} + +void YggdrasilTask::processError(QJsonObject responseData) +{ + QJsonValue errorVal = responseData.value("error"); + QJsonValue errorMessageValue = responseData.value("errorMessage"); + QJsonValue causeVal = responseData.value("cause"); + + if (errorVal.isString() && errorMessageValue.isString()) + { + m_error = std::shared_ptr<Error>(new Error{ + errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")}); + changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); + } + else + { + // Error is not in standard format. Don't set m_error and return unknown error. + changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); + } +} + +QString YggdrasilTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_CREATED: + return "Waiting..."; + case STATE_SENDING_REQUEST: + return tr("Sending request to auth servers..."); + case STATE_PROCESSING_RESPONSE: + return tr("Processing response from servers..."); + case STATE_SUCCEEDED: + return tr("Authentication task succeeded."); + case STATE_FAILED_SOFT: + return tr("Failed to contact the authentication server."); + case STATE_FAILED_HARD: + return tr("Failed to authenticate."); + default: + return tr("..."); + } +} + +void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason) +{ + m_state = newState; + setStatus(getStateMessage()); + if (newState == STATE_SUCCEEDED) + { + emitSucceeded(); + } + else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT) + { + emitFailed(reason); + } +} + +YggdrasilTask::State YggdrasilTask::state() +{ + return m_state; +} diff --git a/api/logic/minecraft/auth/YggdrasilTask.h b/api/logic/minecraft/auth/YggdrasilTask.h new file mode 100644 index 00000000..c84cfc06 --- /dev/null +++ b/api/logic/minecraft/auth/YggdrasilTask.h @@ -0,0 +1,150 @@ +/* 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 <tasks/Task.h> + +#include <QString> +#include <QJsonObject> +#include <QTimer> +#include <qsslerror.h> + +#include "MojangAccount.h" + +class QNetworkReply; + +/** + * A Yggdrasil task is a task that performs an operation on a given mojang account. + */ +class YggdrasilTask : public Task +{ + Q_OBJECT +public: + explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); + + /** + * assign a session to this task. the session will be filled with required infomration + * upon completion + */ + void assignSession(AuthSessionPtr session) + { + m_session = session; + } + + /// get the assigned session for filling with information. + AuthSessionPtr getAssignedSession() + { + return m_session; + } + + /** + * Class describing a Yggdrasil error response. + */ + struct Error + { + QString m_errorMessageShort; + QString m_errorMessageVerbose; + QString m_cause; + }; + + enum AbortedBy + { + BY_NOTHING, + BY_USER, + BY_TIMEOUT + } m_aborted = BY_NOTHING; + + /** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ + enum State + { + STATE_CREATED, + STATE_SENDING_REQUEST, + STATE_PROCESSING_RESPONSE, + STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated + STATE_FAILED_HARD, //!< hard failure. auth is invalid + STATE_SUCCEEDED + } m_state = STATE_CREATED; + +protected: + + virtual void executeTask() override; + + /** + * Gets the JSON object that will be sent to the authentication server. + * Should be overridden by subclasses. + */ + virtual QJsonObject getRequestContent() const = 0; + + /** + * Gets the endpoint to POST to. + * No leading slash. + */ + virtual QString getEndpoint() const = 0; + + /** + * Processes the response received from the server. + * If an error occurred, this should emit a failed signal and return false. + * If Yggdrasil gave an error response, it should call setError() first, and then return false. + * Otherwise, it should return true. + * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with + * an empty QJsonObject. + */ + virtual void processResponse(QJsonObject responseData) = 0; + + /** + * Processes an error response received from the server. + * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. + * \returns a QString error message that will be passed to emitFailed. + */ + virtual void processError(QJsonObject responseData); + + /** + * Returns the state message for the given state. + * Used to set the status message for the task. + * Should be overridden by subclasses that want to change messages for a given state. + */ + virtual QString getStateMessage() const; + +protected +slots: + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + void sslErrors(QList<QSslError>); + + void changeState(State newState, QString reason=QString()); +public +slots: + virtual bool abort() override; + void abortByTimeout(); + State state(); +protected: + // FIXME: segfault disaster waiting to happen + MojangAccount *m_account = nullptr; + QNetworkReply *m_netReply = nullptr; + std::shared_ptr<Error> m_error; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 30000; + const int time_step = 50; + + AuthSessionPtr m_session; +}; diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp new file mode 100644 index 00000000..8d136f0b --- /dev/null +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp @@ -0,0 +1,202 @@ + +/* 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 "AuthenticateTask.h" +#include "../MojangAccount.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QVariant> + +#include <QDebug> +#include <QUuid> + +AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password, + QObject *parent) + : YggdrasilTask(account, parent), m_password(password) +{ +} + +QJsonObject AuthenticateTask::getRequestContent() const +{ + /* + * { + * "agent": { // optional + * "name": "Minecraft", // So far this is the only encountered value + * "version": 1 // This number might be increased + * // by the vanilla client in the future + * }, + * "username": "mojang account name", // Can be an email address or player name for + // unmigrated accounts + * "password": "mojang account password", + * "clientToken": "client identifier" // optional + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + + { + QJsonObject agent; + // C++ makes string literals void* for some stupid reason, so we have to tell it + // QString... Thanks Obama. + agent.insert("name", QString("Minecraft")); + agent.insert("version", 1); + req.insert("agent", agent); + } + + req.insert("username", m_account->username()); + req.insert("password", m_password); + req.insert("requestUser", true); + + // If we already have a client token, give it to the server. + // Otherwise, let the server give us one. + + if(m_account->m_clientToken.isEmpty()) + { + auto uuid = QUuid::createUuid(); + auto uuidString = uuid.toString().remove('{').remove('-').remove('}'); + m_account->m_clientToken = uuidString; + } + req.insert("clientToken", m_account->m_clientToken); + + return req; +} + +void AuthenticateTask::processResponse(QJsonObject responseData) +{ + // Read the response data. We need to get the client token, access token, and the selected + // profile. + qDebug() << "Processing authentication response."; + // qDebug() << responseData; + // If we already have a client token, make sure the one the server gave us matches our + // existing one. + qDebug() << "Getting client token."; + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) + { + // Fail if the server gave us an empty client token + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + return; + } + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) + { + changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + return; + } + // Set the client token. + m_account->m_clientToken = clientToken; + + // Now, we set the access token. + qDebug() << "Getting access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) + { + // Fail if the server didn't give us an access token. + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + return; + } + // Set the access token. + m_account->m_accessToken = accessToken; + + // Now we load the list of available profiles. + // Mojang hasn't yet implemented the profile system, + // but we might as well support what's there so we + // don't have trouble implementing it later. + qDebug() << "Loading profile list."; + QJsonArray availableProfiles = responseData.value("availableProfiles").toArray(); + QList<AccountProfile> loadedProfiles; + for (auto iter : availableProfiles) + { + QJsonObject profile = iter.toObject(); + // Profiles are easy, we just need their ID and name. + QString id = profile.value("id").toString(""); + QString name = profile.value("name").toString(""); + bool legacy = profile.value("legacy").toBool(false); + + if (id.isEmpty() || name.isEmpty()) + { + // This should never happen, but we might as well + // warn about it if it does so we can debug it easily. + // You never know when Mojang might do something truly derpy. + qWarning() << "Found entry in available profiles list with missing ID or name " + "field. Ignoring it."; + } + + // Now, add a new AccountProfile entry to the list. + loadedProfiles.append({id, name, legacy}); + } + // Put the list of profiles we loaded into the MojangAccount object. + m_account->m_profiles = loadedProfiles; + + // Finally, we set the current profile to the correct value. This is pretty simple. + // We do need to make sure that the current profile that the server gave us + // is actually in the available profiles list. + // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know). + qDebug() << "Setting current profile."; + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (currentProfileId.isEmpty()) + { + changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium.")); + return; + } + if (!m_account->setCurrentProfile(currentProfileId)) + { + changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list.")); + return; + } + + // this is what the vanilla launcher passes to the userProperties launch param + if (responseData.contains("user")) + { + User u; + auto obj = responseData.value("user").toObject(); + u.id = obj.value("id").toString(); + auto propArray = obj.value("properties").toArray(); + for (auto prop : propArray) + { + auto propTuple = prop.toObject(); + auto name = propTuple.value("name").toString(); + auto value = propTuple.value("value").toString(); + u.properties.insert(name, value); + } + m_account->m_user = u; + } + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading authentication response."; + changeState(STATE_SUCCEEDED); +} + +QString AuthenticateTask::getEndpoint() const +{ + return "authenticate"; +} + +QString AuthenticateTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_SENDING_REQUEST: + return tr("Authenticating: Sending request..."); + case STATE_PROCESSING_RESPONSE: + return tr("Authenticating: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.h b/api/logic/minecraft/auth/flows/AuthenticateTask.h new file mode 100644 index 00000000..398fab98 --- /dev/null +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.h @@ -0,0 +1,46 @@ +/* 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 "../YggdrasilTask.h" + +#include <QObject> +#include <QString> +#include <QJsonObject> + +/** + * The authenticate task takes a MojangAccount with no access token and password and attempts to + * authenticate with Mojang's servers. + * If successful, it will set the MojangAccount's access token. + */ +class AuthenticateTask : public YggdrasilTask +{ + Q_OBJECT +public: + AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; + +private: + QString m_password; +}; diff --git a/api/logic/minecraft/auth/flows/RefreshTask.cpp b/api/logic/minecraft/auth/flows/RefreshTask.cpp new file mode 100644 index 00000000..a0fb2e48 --- /dev/null +++ b/api/logic/minecraft/auth/flows/RefreshTask.cpp @@ -0,0 +1,144 @@ +/* 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 "RefreshTask.h" +#include "../MojangAccount.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QVariant> + +#include <QDebug> + +RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account) +{ +} + +QJsonObject RefreshTask::getRequestContent() const +{ + /* + * { + * "clientToken": "client identifier" + * "accessToken": "current access token to be refreshed" + * "selectedProfile": // specifying this causes errors + * { + * "id": "profile ID" + * "name": "profile name" + * } + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + req.insert("clientToken", m_account->m_clientToken); + req.insert("accessToken", m_account->m_accessToken); + /* + { + auto currentProfile = m_account->currentProfile(); + QJsonObject profile; + profile.insert("id", currentProfile->id()); + profile.insert("name", currentProfile->name()); + req.insert("selectedProfile", profile); + } + */ + req.insert("requestUser", true); + + return req; +} + +void RefreshTask::processResponse(QJsonObject responseData) +{ + // Read the response data. We need to get the client token, access token, and the selected + // profile. + qDebug() << "Processing authentication response."; + + // qDebug() << responseData; + // If we already have a client token, make sure the one the server gave us matches our + // existing one. + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) + { + // Fail if the server gave us an empty client token + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + return; + } + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) + { + changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + return; + } + + // Now, we set the access token. + qDebug() << "Getting new access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) + { + // Fail if the server didn't give us an access token. + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + return; + } + + // we validate that the server responded right. (our current profile = returned current + // profile) + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (m_account->currentProfile()->id != currentProfileId) + { + changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected.")); + return; + } + + // this is what the vanilla launcher passes to the userProperties launch param + if (responseData.contains("user")) + { + User u; + auto obj = responseData.value("user").toObject(); + u.id = obj.value("id").toString(); + auto propArray = obj.value("properties").toArray(); + for (auto prop : propArray) + { + auto propTuple = prop.toObject(); + auto name = propTuple.value("name").toString(); + auto value = propTuple.value("value").toString(); + u.properties.insert(name, value); + } + m_account->m_user = u; + } + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading refresh response."; + // Reset the access token. + m_account->m_accessToken = accessToken; + changeState(STATE_SUCCEEDED); +} + +QString RefreshTask::getEndpoint() const +{ + return "refresh"; +} + +QString RefreshTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_SENDING_REQUEST: + return tr("Refreshing login token..."); + case STATE_PROCESSING_RESPONSE: + return tr("Refreshing login token: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/api/logic/minecraft/auth/flows/RefreshTask.h b/api/logic/minecraft/auth/flows/RefreshTask.h new file mode 100644 index 00000000..17714b4f --- /dev/null +++ b/api/logic/minecraft/auth/flows/RefreshTask.h @@ -0,0 +1,44 @@ +/* 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 "../YggdrasilTask.h" + +#include <QObject> +#include <QString> +#include <QJsonObject> + +/** + * The authenticate task takes a MojangAccount with a possibly timed-out access token + * and attempts to authenticate with Mojang's servers. + * If successful, it will set the new access token. The token is considered validated. + */ +class RefreshTask : public YggdrasilTask +{ + Q_OBJECT +public: + RefreshTask(MojangAccount * account); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; +}; + diff --git a/api/logic/minecraft/auth/flows/ValidateTask.cpp b/api/logic/minecraft/auth/flows/ValidateTask.cpp new file mode 100644 index 00000000..4deceb6a --- /dev/null +++ b/api/logic/minecraft/auth/flows/ValidateTask.cpp @@ -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. + */ + +#include "ValidateTask.h" +#include "../MojangAccount.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QVariant> + +#include <QDebug> + +ValidateTask::ValidateTask(MojangAccount * account, QObject *parent) + : YggdrasilTask(account, parent) +{ +} + +QJsonObject ValidateTask::getRequestContent() const +{ + QJsonObject req; + req.insert("accessToken", m_account->m_accessToken); + return req; +} + +void ValidateTask::processResponse(QJsonObject responseData) +{ + // Assume that if processError wasn't called, then the request was successful. + changeState(YggdrasilTask::STATE_SUCCEEDED); +} + +QString ValidateTask::getEndpoint() const +{ + return "validate"; +} + +QString ValidateTask::getStateMessage() const +{ + switch (m_state) + { + case YggdrasilTask::STATE_SENDING_REQUEST: + return tr("Validating access token: Sending request..."); + case YggdrasilTask::STATE_PROCESSING_RESPONSE: + return tr("Validating access token: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/api/logic/minecraft/auth/flows/ValidateTask.h b/api/logic/minecraft/auth/flows/ValidateTask.h new file mode 100644 index 00000000..77d628a0 --- /dev/null +++ b/api/logic/minecraft/auth/flows/ValidateTask.h @@ -0,0 +1,47 @@ +/* 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. + */ + +/* + * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME: + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include <QObject> +#include <QString> +#include <QJsonObject> + +/** + * The validate task takes a MojangAccount and checks to make sure its access token is valid. + */ +class ValidateTask : public YggdrasilTask +{ + Q_OBJECT +public: + ValidateTask(MojangAccount *account, QObject *parent = 0); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; + +private: +}; diff --git a/api/logic/minecraft/forge/ForgeInstaller.cpp b/api/logic/minecraft/forge/ForgeInstaller.cpp new file mode 100644 index 00000000..353328ab --- /dev/null +++ b/api/logic/minecraft/forge/ForgeInstaller.cpp @@ -0,0 +1,458 @@ +/* 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/onesix/OneSixVersionFormat.h> +#include "minecraft/VersionFilterData.h" +#include "minecraft/MinecraftVersion.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) +{ + VersionFilePtr 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; + + try + { + newVersion = OneSixVersionFormat::versionFileFromJson(QJsonDocument(versionInfoVal.toObject()), QString(), false); + } + catch(Exception &err) + { + qWarning() << "Forge: Fatal error while parsing version file:" << err.what(); + return; + } + + for(auto problem: newVersion->getProblems()) + { + qWarning() << "Forge: Problem found: " << problem.getDescription(); + } + if(newVersion->getProblemSeverity() == ProblemSeverity::PROBLEM_ERROR) + { + qWarning() << "Forge: Errors found while parsing version file"; + return; + } + + QJsonObject installObj = installVal.toObject(); + QString libraryName = installObj.value("path").toString(); + internalPath = installObj.value("filePath").toString(); + m_forgeVersionString = installObj.value("version").toString().remove("Forge", Qt::CaseInsensitive).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->setStale(false); + cacheentry->setMD5Sum(md5sum.result().toHex().constData()); + ENV.metacache()->updateEntry(cacheentry); + } + file.close(); + + m_forge_json = newVersion; +} + +bool ForgeInstaller::add(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + + if (!m_forge_json) + { + return false; + } + + // A blacklist + QSet<QString> blacklist{"authlib", "realms"}; + QList<QString> xzlist{"org.scala-lang", "com.typesafe"}; + + // get the minecraft version from the instance + VersionFilePtr minecraft; + auto minecraftPatch = to->getMinecraftProfile()->versionPatch("net.minecraft"); + if(minecraftPatch) + { + minecraft = std::dynamic_pointer_cast<VersionFile>(minecraftPatch); + if(!minecraft) + { + auto mcWrap = std::dynamic_pointer_cast<MinecraftVersion>(minecraftPatch); + if(mcWrap) + { + minecraft = mcWrap->getVersionFile(); + } + } + } + + // for each library in the version we are adding (except for the blacklisted) + QMutableListIterator<LibraryPtr> iter(m_forge_json->libraries); + while (iter.hasNext()) + { + auto library = iter.next(); + QString libName = library->artifactId(); + QString libVersion = library->version(); + QString rawName = library->rawName(); + + // ignore lwjgl libraries. + if (g_VersionFilterData.lwjglWhitelist.contains(library->artifactPrefix())) + { + iter.remove(); + continue; + } + // ignore other blacklisted (realms, authlib) + if (blacklist.contains(libName)) + { + iter.remove(); + continue; + } + // if minecraft version was found, ignore everything that is already in the minecraft version + if(minecraft) + { + bool found = false; + for (auto & lib: minecraft->libraries) + { + if(library->artifactPrefix() == lib->artifactPrefix() && library->version() == lib->version()) + { + found = true; + break; + } + } + if (found) + continue; + } + + // if this is the actual forge lib, set an absolute url for the download + if (m_forge_version->type == ForgeVersion::Gradle) + { + if (libName == "forge") + { + library->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)); + library->setRawName(spec); + } + } + else + { + if (libName.contains("minecraftforge")) + { + library->setAbsoluteUrl(m_universal_url); + } + } + + // mark bad libraries based on the xzlist above + for (auto entry : xzlist) + { + qDebug() << "Testing " << rawName << " : " << entry; + if (rawName.startsWith(entry)) + { + library->setHint("forge-pack-xz"); + break; + } + } + } + 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(tweakers.size()) + { + args.operator=(args.trimmed()); + m_forge_json->addTweakers = tweakers; + } + } + if(minecraft && args == minecraft->minecraftArguments) + { + args.clear(); + } + + m_forge_json->name = "Forge"; + m_forge_json->fileId = id(); + m_forge_json->version = m_forgeVersionString; + m_forge_json->dependsOnMinecraftVersion = to->intendedVersionId(); + m_forge_json->order = 5; + + // reset some things we do not want to be passed along. + m_forge_json->m_releaseTime = QDateTime(); + m_forge_json->m_updateTime = QDateTime(); + m_forge_json->minimumLauncherVersion = -1; + m_forge_json->type.clear(); + m_forge_json->minecraftArguments.clear(); + m_forge_json->minecraftVersion.clear(); + + QSaveFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(m_forge_json, true).toJson()); + file.commit(); + + 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->isStale()) + { + QFileInfo localFile(entry->getFullPath()); + if (localFile.size() <= 0x4000) + { + entry->setStale(true); + } + } + + if (entry->isStale()) + { + 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/api/logic/minecraft/forge/ForgeInstaller.h b/api/logic/minecraft/forge/ForgeInstaller.h new file mode 100644 index 00000000..499a6fb3 --- /dev/null +++ b/api/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 VersionFile; +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<VersionFile> 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/api/logic/minecraft/forge/ForgeVersion.cpp b/api/logic/minecraft/forge/ForgeVersion.cpp new file mode 100644 index 00000000..b859a28c --- /dev/null +++ b/api/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/api/logic/minecraft/forge/ForgeVersion.h b/api/logic/minecraft/forge/ForgeVersion.h new file mode 100644 index 00000000..e77d32f1 --- /dev/null +++ b/api/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/api/logic/minecraft/forge/ForgeVersionList.cpp b/api/logic/minecraft/forge/ForgeVersionList.cpp new file mode 100644 index 00000000..de185e5f --- /dev/null +++ b/api/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(); + } +} + +BaseVersionList::RoleList ForgeVersionList::providesRoles() const +{ + 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->setStale(true); + gradleForgeListEntry->setStale(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/api/logic/minecraft/forge/ForgeVersionList.h b/api/logic/minecraft/forge/ForgeVersionList.h new file mode 100644 index 00000000..62c08b2a --- /dev/null +++ b/api/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 RoleList providesRoles() const 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/api/logic/minecraft/forge/ForgeXzDownload.cpp b/api/logic/minecraft/forge/ForgeXzDownload.cpp new file mode 100644 index 00000000..adf96552 --- /dev/null +++ b/api/logic/minecraft/forge/ForgeXzDownload.cpp @@ -0,0 +1,358 @@ +/* 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; + m_url = "http://files.minecraftforge.net/maven/" + m_url_path + ".pack.xz"; +} + +void ForgeXzDownload::start() +{ + m_status = Job_InProgress; + if (!m_entry->isStale()) + { + 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; + } + + qDebug() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().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; + emit failed(m_index_within_job); +} + +void ForgeXzDownload::downloadFinished() +{ + // 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> +#include <unistd.h> + +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; + } + int handle_in_dup = dup (handle_in); + if(handle_in_dup == -1) + { + qCritical() << "Error reopening " << pack200_file.fileName(); + failAndTryNextMirror(); + return; + } + FILE *file_in = fdopen (handle_in_dup, "rb"); + 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; + } + int handle_out_dup = dup (handle_out); + if(handle_out_dup == -1) + { + qCritical() << "Error reopening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + FILE *file_out = fdopen (handle_out_dup, "wb"); + if(!file_out) + { + qCritical() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + try + { + // NOTE: this takes ownership of both FILE pointers. That's why we duplicate them above. + 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; + } + auto hash = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5); + m_entry->setMD5Sum(hash.toHex().constData()); + jar_file.close(); + + QFileInfo output_file_info(m_target_path); + m_entry->setETag(m_reply->rawHeader("ETag").constData()); + m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); + m_entry->setStale(false); + ENV.metacache()->updateEntry(m_entry); + + m_reply.reset(); + emit succeeded(m_index_within_job); +} diff --git a/api/logic/minecraft/forge/ForgeXzDownload.h b/api/logic/minecraft/forge/ForgeXzDownload.h new file mode 100644 index 00000000..67524405 --- /dev/null +++ b/api/logic/minecraft/forge/ForgeXzDownload.h @@ -0,0 +1,59 @@ +/* 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> + +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; + /// 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(){}; + +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(); +}; diff --git a/api/logic/minecraft/forge/LegacyForge.cpp b/api/logic/minecraft/forge/LegacyForge.cpp new file mode 100644 index 00000000..aa2c8063 --- /dev/null +++ b/api/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/api/logic/minecraft/forge/LegacyForge.h b/api/logic/minecraft/forge/LegacyForge.h new file mode 100644 index 00000000..f51d5e85 --- /dev/null +++ b/api/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); +}; diff --git a/api/logic/minecraft/ftb/FTBPlugin.cpp b/api/logic/minecraft/ftb/FTBPlugin.cpp new file mode 100644 index 00000000..a142c106 --- /dev/null +++ b/api/logic/minecraft/ftb/FTBPlugin.cpp @@ -0,0 +1,395 @@ +#include "FTBPlugin.h" +#include <Env.h> +#include "FTBVersion.h" +#include "LegacyFTBInstance.h" +#include "OneSixFTBInstance.h" +#include <BaseInstance.h> +#include <InstanceList.h> +#include <minecraft/MinecraftVersionList.h> +#include <settings/INISettingsObject.h> +#include <FileSystem.h> +#include "QDebug" +#include <QXmlStreamReader> +#include <QRegularExpression> + +struct FTBRecord +{ + QString dirName; + QString name; + QString logo; + QString iconKey; + QString mcVersion; + QString description; + QString instanceDir; + QString templateDir; + bool operator==(const FTBRecord other) const + { + return instanceDir == other.instanceDir; + } +}; + +inline uint qHash(FTBRecord record) +{ + return qHash(record.instanceDir); +} + +QSet<FTBRecord> discoverFTBInstances(SettingsObjectPtr globalSettings) +{ + QSet<FTBRecord> records; + QDir dir = QDir(globalSettings->get("FTBLauncherLocal").toString()); + QDir dataDir = QDir(globalSettings->get("FTBRoot").toString()); + if (!dataDir.exists()) + { + qDebug() << "The FTB directory specified does not exist. Please check your settings"; + return records; + } + else if (!dir.exists()) + { + qDebug() << "The FTB launcher data directory specified does not exist. Please check " + "your settings"; + return records; + } + dir.cd("ModPacks"); + auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name); + for (auto filename : allFiles) + { + if (!filename.endsWith(".xml")) + continue; + auto fpath = dir.absoluteFilePath(filename); + QFile f(fpath); + qDebug() << "Discovering FTB instances -- " << fpath; + if (!f.open(QFile::ReadOnly)) + continue; + + // read the FTB packs XML. + QXmlStreamReader reader(&f); + while (!reader.atEnd()) + { + switch (reader.readNext()) + { + case QXmlStreamReader::StartElement: + { + if (reader.name() == "modpack") + { + QXmlStreamAttributes attrs = reader.attributes(); + FTBRecord record; + record.dirName = attrs.value("dir").toString(); + record.instanceDir = dataDir.absoluteFilePath(record.dirName); + record.templateDir = dir.absoluteFilePath(record.dirName); + QDir test(record.instanceDir); + qDebug() << dataDir.absolutePath() << record.instanceDir << record.dirName; + if (!test.exists()) + continue; + record.name = attrs.value("name").toString(); + record.logo = attrs.value("logo").toString(); + QString logo = record.logo; + record.iconKey = logo.remove(QRegularExpression("\\..*")); + auto customVersions = attrs.value("customMCVersions"); + if (!customVersions.isNull()) + { + QMap<QString, QString> versionMatcher; + QString customVersionsStr = customVersions.toString(); + QStringList list = customVersionsStr.split(';'); + for (auto item : list) + { + auto segment = item.split('^'); + if (segment.size() != 2) + { + qCritical() << "FTB: Segment of size < 2 in " + << customVersionsStr; + continue; + } + versionMatcher[segment[0]] = segment[1]; + } + auto actualVersion = attrs.value("version").toString(); + if (versionMatcher.contains(actualVersion)) + { + record.mcVersion = versionMatcher[actualVersion]; + } + else + { + record.mcVersion = attrs.value("mcVersion").toString(); + } + } + else + { + record.mcVersion = attrs.value("mcVersion").toString(); + } + record.description = attrs.value("description").toString(); + records.insert(record); + } + break; + } + case QXmlStreamReader::EndElement: + break; + case QXmlStreamReader::Characters: + break; + default: + break; + } + } + f.close(); + } + return records; +} + +InstancePtr loadInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record) +{ + InstancePtr inst; + + auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg")); + m_settings->registerSetting("InstanceType", "Legacy"); + + qDebug() << "Loading existing " << record.name; + + QString inst_type = m_settings->get("InstanceType").toString(); + if (inst_type == "LegacyFTB") + { + inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir)); + } + else if (inst_type == "OneSixFTB") + { + inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir)); + } + else + { + return nullptr; + } + qDebug() << "Construction " << record.instanceDir; + + SettingsObject::Lock lock(inst->settings()); + inst->init(); + qDebug() << "Init " << record.instanceDir; + inst->setGroupInitial("FTB"); + /** + * FIXME: this does not respect the user's preferences. BUT, it would work nicely with the planned pack support + * -> instead of changing the user values, change pack values (defaults you can look at and revert to) + */ + /* + inst->setName(record.name); + inst->setIconKey(record.iconKey); + inst->setNotes(record.description); + */ + if (inst->intendedVersionId() != record.mcVersion) + { + inst->setIntendedVersionId(record.mcVersion); + } + qDebug() << "Post-Process " << record.instanceDir; + if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap)) + { + return nullptr; + } + qDebug() << "Final " << record.instanceDir; + return inst; +} + +InstancePtr createInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record) +{ + QDir rootDir(record.instanceDir); + + InstancePtr inst; + + qDebug() << "Converting " << record.name << " as new."; + + auto mcVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", record.mcVersion)); + if (!mcVersion) + { + qCritical() << "Can't load instance " << record.instanceDir + << " because minecraft version " << record.mcVersion + << " can't be resolved."; + return nullptr; + } + + if (!rootDir.exists() && !rootDir.mkpath(".")) + { + qCritical() << "Can't create instance folder" << record.instanceDir; + return nullptr; + } + + auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg")); + m_settings->registerSetting("InstanceType", "Legacy"); + + if (mcVersion->usesLegacyLauncher()) + { + m_settings->set("InstanceType", "LegacyFTB"); + inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir)); + } + else + { + m_settings->set("InstanceType", "OneSixFTB"); + inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir)); + } + // initialize + { + SettingsObject::Lock lock(inst->settings()); + inst->setIntendedVersionId(mcVersion->descriptor()); + inst->init(); + inst->setGroupInitial("FTB"); + inst->setName(record.name); + inst->setIconKey(record.iconKey); + inst->setNotes(record.description); + qDebug() << "Post-Process " << record.instanceDir; + if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap)) + { + return nullptr; + } + } + return inst; +} + +void FTBPlugin::loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList) +{ + // nothing to load when we don't have + if (globalSettings->get("TrackFTBInstances").toBool() != true) + { + return; + } + + auto records = discoverFTBInstances(globalSettings); + if (!records.size()) + { + qDebug() << "No FTB instances to load."; + return; + } + qDebug() << "Loading FTB instances! -- got " << records.size(); + // process the records we acquired. + for (auto record : records) + { + qDebug() << "Loading FTB instance from " << record.instanceDir; + QString iconKey = record.iconKey; + // MMC->icons()->addIcon(iconKey, iconKey, FS::PathCombine(record.templateDir, record.logo), MMCIcon::Transient); + auto settingsFilePath = FS::PathCombine(record.instanceDir, "instance.cfg"); + qDebug() << "ICON get!"; + + if (QFileInfo(settingsFilePath).exists()) + { + auto instPtr = loadInstance(globalSettings, groupMap, record); + if (!instPtr) + { + qWarning() << "Couldn't load instance config:" << settingsFilePath; + if(!QFile::remove(settingsFilePath)) + { + qWarning() << "Couldn't remove broken instance config!"; + continue; + } + // failed to load, but removed the poisonous file + } + else + { + tempList.append(InstancePtr(instPtr)); + continue; + } + } + auto instPtr = createInstance(globalSettings, groupMap, record); + if (!instPtr) + { + qWarning() << "Couldn't create FTB instance!"; + continue; + } + tempList.append(InstancePtr(instPtr)); + } +} + +#ifdef Q_OS_WIN32 +#include <windows.h> +static const int APPDATA_BUFFER_SIZE = 1024; +#endif + +static QString getLocalCacheStorageLocation() +{ + QString ftbDefault; +#ifdef Q_OS_WIN32 + wchar_t buf[APPDATA_BUFFER_SIZE]; + if (GetEnvironmentVariableW(L"LOCALAPPDATA", buf, APPDATA_BUFFER_SIZE)) // local + { + ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher"); + } + else if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE)) // roaming + { + ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher"); + } + else + { + qCritical() << "Your LOCALAPPDATA and APPDATA folders are missing!" + " If you are on windows, this means your system is broken."; + } +#elif defined(Q_OS_MAC) + ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher"); +#else + ftbDefault = QDir::home().absoluteFilePath(".ftblauncher"); +#endif + return ftbDefault; +} + + +static QString getRoamingStorageLocation() +{ + QString ftbDefault; +#ifdef Q_OS_WIN32 + wchar_t buf[APPDATA_BUFFER_SIZE]; + QString cacheStorage; + if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE)) + { + ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher"); + } + else + { + qCritical() << "Your APPDATA folder is missing! If you are on windows, this means your system is broken."; + } +#elif defined(Q_OS_MAC) + ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher"); +#else + ftbDefault = QDir::home().absoluteFilePath(".ftblauncher"); +#endif + return ftbDefault; +} + +void FTBPlugin::initialize(SettingsObjectPtr globalSettings) +{ + // FTB + globalSettings->registerSetting("TrackFTBInstances", false); + QString ftbRoaming = getRoamingStorageLocation(); + QString ftbLocal = getLocalCacheStorageLocation(); + + globalSettings->registerSetting("FTBLauncherRoaming", ftbRoaming); + globalSettings->registerSetting("FTBLauncherLocal", ftbLocal); + qDebug() << "FTB Launcher paths:" << globalSettings->get("FTBLauncherRoaming").toString() + << "and" << globalSettings->get("FTBLauncherLocal").toString(); + + globalSettings->registerSetting("FTBRoot"); + if (globalSettings->get("FTBRoot").isNull()) + { + QString ftbRoot; + QFile f(QDir(globalSettings->get("FTBLauncherRoaming").toString()).absoluteFilePath("ftblaunch.cfg")); + qDebug() << "Attempting to read" << f.fileName(); + if (f.open(QFile::ReadOnly)) + { + const QString data = QString::fromLatin1(f.readAll()); + QRegularExpression exp("installPath=(.*)"); + ftbRoot = QDir::cleanPath(exp.match(data).captured(1)); +#ifdef Q_OS_WIN32 + if (!ftbRoot.isEmpty()) + { + if (ftbRoot.at(0).isLetter() && ftbRoot.size() > 1 && ftbRoot.at(1) == '/') + { + ftbRoot.remove(1, 1); + } + } +#endif + if (ftbRoot.isEmpty()) + { + qDebug() << "Failed to get FTB root path"; + } + else + { + qDebug() << "FTB is installed at" << ftbRoot; + globalSettings->set("FTBRoot", ftbRoot); + } + } + else + { + qWarning() << "Couldn't open" << f.fileName() << ":" << f.errorString(); + qWarning() << "This is perfectly normal if you don't have FTB installed"; + } + } +} diff --git a/api/logic/minecraft/ftb/FTBPlugin.h b/api/logic/minecraft/ftb/FTBPlugin.h new file mode 100644 index 00000000..6851d8a5 --- /dev/null +++ b/api/logic/minecraft/ftb/FTBPlugin.h @@ -0,0 +1,13 @@ +#pragma once + +#include <BaseInstance.h> + +#include "multimc_logic_export.h" + +// Pseudo-plugin for FTB related things. Super derpy! +class MULTIMC_LOGIC_EXPORT FTBPlugin +{ +public: + static void initialize(SettingsObjectPtr globalSettings); + static void loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList); +}; diff --git a/api/logic/minecraft/ftb/FTBProfileStrategy.cpp b/api/logic/minecraft/ftb/FTBProfileStrategy.cpp new file mode 100644 index 00000000..f5faacae --- /dev/null +++ b/api/logic/minecraft/ftb/FTBProfileStrategy.cpp @@ -0,0 +1,128 @@ +#include "FTBProfileStrategy.h" +#include "OneSixFTBInstance.h" + +#include "minecraft/VersionBuildError.h" +#include "minecraft/MinecraftVersionList.h" +#include <FileSystem.h> + +#include <QDir> +#include <QUuid> +#include <QJsonDocument> +#include <QJsonArray> + +FTBProfileStrategy::FTBProfileStrategy(OneSixFTBInstance* instance) : OneSixProfileStrategy(instance) +{ +} + +void FTBProfileStrategy::loadDefaultBuiltinPatches() +{ + // FIXME: this should be here, but it needs us to be able to deal with multiple libraries paths + // OneSixProfileStrategy::loadDefaultBuiltinPatches(); + auto mcVersion = m_instance->intendedVersionId(); + auto nativeInstance = dynamic_cast<OneSixFTBInstance *>(m_instance); + + ProfilePatchPtr minecraftPatch; + { + auto mcJson = m_instance->versionsPath().absoluteFilePath(mcVersion + "/" + mcVersion + ".json"); + // load up the base minecraft patch + if(QFile::exists(mcJson)) + { + auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false); + file->fileId = "net.minecraft"; + file->name = QObject::tr("Minecraft (tracked)"); + file->setVanilla(true); + if(file->version.isEmpty()) + { + file->version = mcVersion; + } + for(auto addLib: file->libraries) + { + addLib->setHint("local"); + addLib->setStoragePrefix(nativeInstance->librariesPath().absolutePath()); + } + minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(file); + } + else + { + throw VersionIncomplete("net.minecraft"); + } + minecraftPatch->setOrder(-2); + } + profile->appendPatch(minecraftPatch); + + ProfilePatchPtr packPatch; + { + auto mcJson = m_instance->minecraftRoot() + "/pack.json"; + // load up the base minecraft patch + if(QFile::exists(mcJson)) + { + auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false); + + // adapt the loaded file - the FTB patch file format is different than ours. + file->minecraftVersion.clear(); + for(auto addLib: file->libraries) + { + addLib->setHint("local"); + addLib->setStoragePrefix(nativeInstance->librariesPath().absolutePath()); + } + file->fileId = "org.multimc.ftb.pack"; + file->setVanilla(true); + file->name = QObject::tr("%1 (FTB pack)").arg(m_instance->name()); + if(file->version.isEmpty()) + { + file->version = QObject::tr("Unknown"); + QFile versionFile (FS::PathCombine(m_instance->instanceRoot(), "version")); + if(versionFile.exists()) + { + if(versionFile.open(QIODevice::ReadOnly)) + { + // FIXME: just guessing the encoding/charset here. + auto version = QString::fromUtf8(versionFile.readAll()); + file->version = version; + } + } + } + packPatch = std::dynamic_pointer_cast<ProfilePatch>(file); + } + else + { + throw VersionIncomplete("org.multimc.ftb.pack"); + } + packPatch->setOrder(1); + } + profile->appendPatch(packPatch); + +} + +void FTBProfileStrategy::load() +{ + profile->clearPatches(); + + loadDefaultBuiltinPatches(); + loadUserPatches(); +} + +bool FTBProfileStrategy::saveOrder(ProfileUtils::PatchOrder order) +{ + return false; +} + +bool FTBProfileStrategy::resetOrder() +{ + return false; +} + +bool FTBProfileStrategy::installJarMods(QStringList filepaths) +{ + return false; +} + +bool FTBProfileStrategy::customizePatch(ProfilePatchPtr patch) +{ + return false; +} + +bool FTBProfileStrategy::revertPatch(ProfilePatchPtr patch) +{ + return false; +} diff --git a/api/logic/minecraft/ftb/FTBProfileStrategy.h b/api/logic/minecraft/ftb/FTBProfileStrategy.h new file mode 100644 index 00000000..522af098 --- /dev/null +++ b/api/logic/minecraft/ftb/FTBProfileStrategy.h @@ -0,0 +1,21 @@ +#pragma once +#include "minecraft/ProfileStrategy.h" +#include "minecraft/onesix/OneSixProfileStrategy.h" + +class OneSixFTBInstance; + +class FTBProfileStrategy : public OneSixProfileStrategy +{ +public: + FTBProfileStrategy(OneSixFTBInstance * instance); + virtual ~FTBProfileStrategy() {}; + virtual void load() override; + virtual bool resetOrder() override; + virtual bool saveOrder(ProfileUtils::PatchOrder order) override; + virtual bool installJarMods(QStringList filepaths) override; + virtual bool customizePatch (ProfilePatchPtr patch) override; + virtual bool revertPatch (ProfilePatchPtr patch) override; + +protected: + virtual void loadDefaultBuiltinPatches() override; +}; diff --git a/api/logic/minecraft/ftb/FTBVersion.h b/api/logic/minecraft/ftb/FTBVersion.h new file mode 100644 index 00000000..805319b4 --- /dev/null +++ b/api/logic/minecraft/ftb/FTBVersion.h @@ -0,0 +1,32 @@ +#pragma once +#include <minecraft/MinecraftVersion.h> + +class FTBVersion : public BaseVersion +{ +public: + FTBVersion(MinecraftVersionPtr parent) : m_version(parent){}; + +public: + virtual QString descriptor() override + { + return m_version->descriptor(); + } + + virtual QString name() override + { + return m_version->name(); + } + + virtual QString typeString() const override + { + return m_version->typeString(); + } + + MinecraftVersionPtr getMinecraftVersion() + { + return m_version; + } + +private: + MinecraftVersionPtr m_version; +}; diff --git a/api/logic/minecraft/ftb/LegacyFTBInstance.cpp b/api/logic/minecraft/ftb/LegacyFTBInstance.cpp new file mode 100644 index 00000000..a7091f1d --- /dev/null +++ b/api/logic/minecraft/ftb/LegacyFTBInstance.cpp @@ -0,0 +1,27 @@ +#include "LegacyFTBInstance.h" +#include <settings/INISettingsObject.h> +#include <QDir> + +LegacyFTBInstance::LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) : + LegacyInstance(globalSettings, settings, rootDir) +{ +} + +QString LegacyFTBInstance::id() const +{ + return "FTB/" + BaseInstance::id(); +} + +void LegacyFTBInstance::copy(const QDir &newDir) +{ + // set the target instance to be plain Legacy + INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg")); + settings_obj.registerSetting("InstanceType", "Legacy"); + QString inst_type = settings_obj.get("InstanceType").toString(); + settings_obj.set("InstanceType", "Legacy"); +} + +QString LegacyFTBInstance::typeName() const +{ + return tr("Legacy FTB"); +} diff --git a/api/logic/minecraft/ftb/LegacyFTBInstance.h b/api/logic/minecraft/ftb/LegacyFTBInstance.h new file mode 100644 index 00000000..7178bca4 --- /dev/null +++ b/api/logic/minecraft/ftb/LegacyFTBInstance.h @@ -0,0 +1,17 @@ +#pragma once + +#include "minecraft/legacy/LegacyInstance.h" + +class LegacyFTBInstance : public LegacyInstance +{ + Q_OBJECT +public: + explicit LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual QString id() const; + virtual void copy(const QDir &newDir); + virtual QString typeName() const; + bool canExport() const override + { + return false; + } +}; diff --git a/api/logic/minecraft/ftb/OneSixFTBInstance.cpp b/api/logic/minecraft/ftb/OneSixFTBInstance.cpp new file mode 100644 index 00000000..81e939a1 --- /dev/null +++ b/api/logic/minecraft/ftb/OneSixFTBInstance.cpp @@ -0,0 +1,138 @@ +#include "OneSixFTBInstance.h" +#include "FTBProfileStrategy.h" + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/GradleSpecifier.h" +#include "tasks/SequentialTask.h" +#include <settings/INISettingsObject.h> +#include <FileSystem.h> + +#include <QJsonArray> + +OneSixFTBInstance::OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) : + OneSixInstance(globalSettings, settings, rootDir) +{ + m_globalSettings = globalSettings; +} + +void OneSixFTBInstance::copy(const QDir &newDir) +{ + QStringList libraryNames; + // create patch file + { + qDebug()<< "Creating patch file for FTB instance..."; + QFile f(minecraftRoot() + "/pack.json"); + if (!f.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << f.fileName() << ":" << f.errorString(); + return; + } + QJsonObject root = QJsonDocument::fromJson(f.readAll()).object(); + QJsonArray libs = root.value("libraries").toArray(); + QJsonArray outLibs; + for (auto lib : libs) + { + QJsonObject libObj = lib.toObject(); + libObj.insert("MMC-hint", QString("local")); + libObj.insert("insert", QString("prepend")); + libraryNames.append(libObj.value("name").toString()); + outLibs.append(libObj); + } + root.remove("libraries"); + root.remove("id"); + + // HACK HACK HACK HACK + // A workaround for a problem in MultiMC, triggered by a historical problem in FTB, + // triggered by Mojang getting their library versions wrong in 1.7.10 + if(intendedVersionId() == "1.7.10") + { + auto insert = [&outLibs, &libraryNames](QString name) + { + QJsonObject libObj; + libObj.insert("insert", QString("replace")); + libObj.insert("name", name); + libraryNames.push_back(name); + outLibs.prepend(libObj); + }; + insert("com.google.guava:guava:16.0"); + insert("org.apache.commons:commons-lang3:3.2.1"); + } + root.insert("+libraries", outLibs); + root.insert("order", 1); + root.insert("fileId", QString("org.multimc.ftb.pack.json")); + root.insert("name", name()); + root.insert("mcVersion", intendedVersionId()); + root.insert("version", intendedVersionId()); + FS::ensureFilePathExists(newDir.absoluteFilePath("patches/ftb.json")); + QFile out(newDir.absoluteFilePath("patches/ftb.json")); + if (!out.open(QFile::WriteOnly | QFile::Truncate)) + { + qCritical() << "Couldn't open" << out.fileName() << ":" << out.errorString(); + return; + } + out.write(QJsonDocument(root).toJson()); + } + // copy libraries + { + qDebug() << "Copying FTB libraries"; + for (auto library : libraryNames) + { + GradleSpecifier lib(library); + const QString out = QDir::current().absoluteFilePath("libraries/" + lib.toPath()); + if (QFile::exists(out)) + { + continue; + } + if (!FS::ensureFilePathExists(out)) + { + qCritical() << "Couldn't create folder structure for" << out; + } + if (!QFile::copy(librariesPath().absoluteFilePath(lib.toPath()), out)) + { + qCritical() << "Couldn't copy" << QString(lib); + } + } + } + // now set the target instance to be plain OneSix + INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg")); + settings_obj.registerSetting("InstanceType", "Legacy"); + QString inst_type = settings_obj.get("InstanceType").toString(); + settings_obj.set("InstanceType", "OneSix"); +} + +QString OneSixFTBInstance::id() const +{ + return "FTB/" + BaseInstance::id(); +} + +QDir OneSixFTBInstance::librariesPath() const +{ + return QDir(m_globalSettings->get("FTBRoot").toString() + "/libraries"); +} + +QDir OneSixFTBInstance::versionsPath() const +{ + return QDir(m_globalSettings->get("FTBRoot").toString() + "/versions"); +} + +bool OneSixFTBInstance::providesVersionFile() const +{ + return true; +} + +void OneSixFTBInstance::createProfile() +{ + m_profile.reset(new MinecraftProfile(new FTBProfileStrategy(this))); +} + +std::shared_ptr<Task> OneSixFTBInstance::createUpdateTask() +{ + return OneSixInstance::createUpdateTask(); +} + +QString OneSixFTBInstance::typeName() const +{ + return tr("OneSix FTB"); +} + +#include "OneSixFTBInstance.moc" diff --git a/api/logic/minecraft/ftb/OneSixFTBInstance.h b/api/logic/minecraft/ftb/OneSixFTBInstance.h new file mode 100644 index 00000000..e7f8f485 --- /dev/null +++ b/api/logic/minecraft/ftb/OneSixFTBInstance.h @@ -0,0 +1,30 @@ +#pragma once + +#include "minecraft/onesix/OneSixInstance.h" + +class OneSixFTBInstance : public OneSixInstance +{ + Q_OBJECT +public: + explicit OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual ~OneSixFTBInstance(){}; + + void copy(const QDir &newDir) override; + + virtual void createProfile() override; + + virtual std::shared_ptr<Task> createUpdateTask() override; + + virtual QString id() const override; + + QDir librariesPath() const override; + QDir versionsPath() const override; + bool providesVersionFile() const override; + virtual QString typeName() const override; + bool canExport() const override + { + return false; + } +private: + SettingsObjectPtr m_globalSettings; +}; diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/api/logic/minecraft/legacy/LegacyInstance.cpp new file mode 100644 index 00000000..f8264f20 --- /dev/null +++ b/api/logic/minecraft/legacy/LegacyInstance.cpp @@ -0,0 +1,453 @@ +/* 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 <QFileInfo> +#include <QDir> +#include <settings/Setting.h> + +#include "LegacyInstance.h" + +#include "minecraft/legacy/LegacyUpdate.h" +#include "launch/LaunchTask.h" +#include <launch/steps/LaunchMinecraft.h> +#include <launch/steps/PostLaunchCommand.h> +#include <launch/steps/ModMinecraftJar.h> +#include <launch/steps/Update.h> +#include <launch/steps/PreLaunchCommand.h> +#include <launch/steps/TextPrint.h> +#include <launch/steps/CheckJava.h> +#include "minecraft/ModList.h" +#include "minecraft/WorldList.h" +#include <MMCZip.h> +#include <FileSystem.h> + +LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : MinecraftInstance(globalSettings, settings, rootDir) +{ + m_lwjglFolderSetting = globalSettings->getSetting("LWJGLDir"); + settings->registerSetting("NeedsRebuild", true); + settings->registerSetting("ShouldUpdate", false); + settings->registerSetting("JarVersion", "Unknown"); + settings->registerSetting("LwjglVersion", "2.9.0"); + settings->registerSetting("IntendedJarVersion", ""); + /* + * custom base jar has no default. it is determined in code... see the accessor methods for + *it + * + * for instances that DO NOT have the CustomBaseJar setting (legacy instances), + * [.]minecraft/bin/mcbackup.jar is the default base jar + */ + settings->registerSetting("UseCustomBaseJar", true); + settings->registerSetting("CustomBaseJar", ""); +} + +QString LegacyInstance::baseJar() const +{ + bool customJar = m_settings->get("UseCustomBaseJar").toBool(); + if (customJar) + { + return customBaseJar(); + } + else + return defaultBaseJar(); +} + +QString LegacyInstance::customBaseJar() const +{ + QString value = m_settings->get("CustomBaseJar").toString(); + if (value.isNull() || value.isEmpty()) + { + return defaultCustomBaseJar(); + } + return value; +} + +void LegacyInstance::setCustomBaseJar(QString val) +{ + if (val.isNull() || val.isEmpty() || val == defaultCustomBaseJar()) + m_settings->reset("CustomBaseJar"); + else + m_settings->set("CustomBaseJar", val); +} + +void LegacyInstance::setShouldUseCustomBaseJar(bool val) +{ + m_settings->set("UseCustomBaseJar", val); +} + +bool LegacyInstance::shouldUseCustomBaseJar() const +{ + return m_settings->get("UseCustomBaseJar").toBool(); +} + + +std::shared_ptr<Task> LegacyInstance::createUpdateTask() +{ + // make sure the jar mods list is initialized by asking for it. + auto list = jarModList(); + // create an update task + return std::shared_ptr<Task>(new LegacyUpdate(this, this)); +} + +std::shared_ptr<LaunchTask> LegacyInstance::createLaunchTask(AuthSessionPtr session) +{ + auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr())); + auto pptr = process.get(); + + // print a header + { + process->appendStep(std::make_shared<TextPrint>(pptr, "Minecraft folder is:\n" + minecraftRoot() + "\n\n", MessageLevel::MultiMC)); + } + { + auto step = std::make_shared<CheckJava>(pptr); + process->appendStep(step); + } + // run pre-launch command if that's needed + if(getPreLaunchCommand().size()) + { + auto step = std::make_shared<PreLaunchCommand>(pptr); + step->setWorkingDirectory(minecraftRoot()); + process->appendStep(step); + } + // if we aren't in offline mode,. + if(session->status != AuthSession::PlayableOffline) + { + process->appendStep(std::make_shared<Update>(pptr)); + } + // if there are any jar mods + if(getJarMods().size()) + { + auto step = std::make_shared<ModMinecraftJar>(pptr); + process->appendStep(step); + } + // actually launch the game + { + auto step = std::make_shared<LaunchMinecraft>(pptr); + step->setWorkingDirectory(minecraftRoot()); + step->setAuthSession(session); + process->appendStep(step); + } + // run post-exit command if that's needed + if(getPostExitCommand().size()) + { + auto step = std::make_shared<PostLaunchCommand>(pptr); + step->setWorkingDirectory(minecraftRoot()); + process->appendStep(step); + } + if (session) + { + process->setCensorFilter(createCensorFilterFromSession(session)); + } + return process; +} + +std::shared_ptr<Task> LegacyInstance::createJarModdingTask() +{ + class JarModTask : public Task + { + public: + explicit JarModTask(std::shared_ptr<LegacyInstance> inst) : Task(nullptr), m_inst(inst) + { + } + virtual void executeTask() + { + if (!m_inst->shouldRebuild()) + { + emitSucceeded(); + return; + } + + // Get the mod list + auto modList = m_inst->getJarMods(); + + QFileInfo runnableJar(m_inst->runnableJar()); + QFileInfo baseJar(m_inst->baseJar()); + bool base_is_custom = m_inst->shouldUseCustomBaseJar(); + + // Nothing to do if there are no jar mods to install, no backup and just the mc jar + if (base_is_custom) + { + // yes, this can happen if the instance only has the runnable jar and not the base jar + // it *could* be assumed that such an instance is vanilla, but that wouldn't be safe + // because that's not something mmc4 guarantees + if (runnableJar.isFile() && !baseJar.exists() && modList.empty()) + { + m_inst->setShouldRebuild(false); + emitSucceeded(); + return; + } + + setStatus(tr("Installing mods: Backing up minecraft.jar ...")); + if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath())) + { + emitFailed("It seems both the active and base jar are gone. A fresh base jar will " + "be used on next run."); + m_inst->setShouldRebuild(true); + m_inst->setShouldUpdate(true); + m_inst->setShouldUseCustomBaseJar(false); + return; + } + } + + if (!baseJar.exists()) + { + emitFailed("The base jar " + baseJar.filePath() + " does not exist"); + return; + } + + if (runnableJar.exists() && !QFile::remove(runnableJar.filePath())) + { + emitFailed("Failed to delete old minecraft.jar"); + return; + } + + setStatus(tr("Installing mods: Opening minecraft.jar ...")); + + QString outputJarPath = runnableJar.filePath(); + QString inputJarPath = baseJar.filePath(); + + if(!MMCZip::createModdedJar(inputJarPath, outputJarPath, modList)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + m_inst->setShouldRebuild(false); + // inst->UpdateVersion(true); + emitSucceeded(); + return; + + } + std::shared_ptr<LegacyInstance> m_inst; + }; + return std::make_shared<JarModTask>(std::dynamic_pointer_cast<LegacyInstance>(shared_from_this())); +} + +QString LegacyInstance::createLaunchScript(AuthSessionPtr session) +{ + QString launchScript; + + // window size + QString windowParams; + if (settings()->get("LaunchMaximized").toBool()) + { + windowParams = "max"; + } + else + { + windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); + } + + QString lwjgl = QDir(m_lwjglFolderSetting->get().toString() + "/" + lwjglVersion()).absolutePath(); + launchScript += "userName " + session->player_name + "\n"; + launchScript += "sessionId " + session->session + "\n"; + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + launchScript += "lwjgl " + lwjgl + "\n"; + launchScript += "launcher legacy\n"; + return launchScript; +} + +void LegacyInstance::cleanupAfterRun() +{ + // FIXME: delete the launcher and icons and whatnot. +} + +std::shared_ptr<ModList> LegacyInstance::coreModList() const +{ + if (!core_mod_list) + { + core_mod_list.reset(new ModList(coreModsDir())); + } + core_mod_list->update(); + return core_mod_list; +} + +std::shared_ptr<ModList> LegacyInstance::jarModList() const +{ + if (!jar_mod_list) + { + auto list = new ModList(jarModsDir(), modListFile()); + connect(list, SIGNAL(changed()), SLOT(jarModsChanged())); + jar_mod_list.reset(list); + } + jar_mod_list->update(); + return jar_mod_list; +} + +QList<Mod> LegacyInstance::getJarMods() const +{ + return jarModList()->allMods(); +} + +void LegacyInstance::jarModsChanged() +{ + qDebug() << "Jar mods of instance " << name() << " have changed. Jar will be rebuilt."; + setShouldRebuild(true); +} + +std::shared_ptr<ModList> LegacyInstance::loaderModList() const +{ + if (!loader_mod_list) + { + loader_mod_list.reset(new ModList(loaderModsDir())); + } + loader_mod_list->update(); + return loader_mod_list; +} + +std::shared_ptr<ModList> LegacyInstance::texturePackList() const +{ + if (!texture_pack_list) + { + texture_pack_list.reset(new ModList(texturePacksDir())); + } + texture_pack_list->update(); + return texture_pack_list; +} + +std::shared_ptr<WorldList> LegacyInstance::worldList() const +{ + if (!m_world_list) + { + m_world_list.reset(new WorldList(savesDir())); + } + return m_world_list; +} + +QString LegacyInstance::jarModsDir() const +{ + return FS::PathCombine(instanceRoot(), "instMods"); +} + +QString LegacyInstance::binDir() const +{ + return FS::PathCombine(minecraftRoot(), "bin"); +} + +QString LegacyInstance::libDir() const +{ + return FS::PathCombine(minecraftRoot(), "lib"); +} + +QString LegacyInstance::savesDir() const +{ + return FS::PathCombine(minecraftRoot(), "saves"); +} + +QString LegacyInstance::loaderModsDir() const +{ + return FS::PathCombine(minecraftRoot(), "mods"); +} + +QString LegacyInstance::coreModsDir() const +{ + return FS::PathCombine(minecraftRoot(), "coremods"); +} + +QString LegacyInstance::resourceDir() const +{ + return FS::PathCombine(minecraftRoot(), "resources"); +} +QString LegacyInstance::texturePacksDir() const +{ + return FS::PathCombine(minecraftRoot(), "texturepacks"); +} + +QString LegacyInstance::runnableJar() const +{ + return FS::PathCombine(binDir(), "minecraft.jar"); +} + +QString LegacyInstance::modListFile() const +{ + return FS::PathCombine(instanceRoot(), "modlist"); +} + +QString LegacyInstance::instanceConfigFolder() const +{ + return FS::PathCombine(minecraftRoot(), "config"); +} + +bool LegacyInstance::shouldRebuild() const +{ + return m_settings->get("NeedsRebuild").toBool(); +} + +void LegacyInstance::setShouldRebuild(bool val) +{ + m_settings->set("NeedsRebuild", val); +} + +QString LegacyInstance::currentVersionId() const +{ + return m_settings->get("JarVersion").toString(); +} + +QString LegacyInstance::lwjglVersion() const +{ + return m_settings->get("LwjglVersion").toString(); +} + +void LegacyInstance::setLWJGLVersion(QString val) +{ + m_settings->set("LwjglVersion", val); +} + +QString LegacyInstance::intendedVersionId() const +{ + return m_settings->get("IntendedJarVersion").toString(); +} + +bool LegacyInstance::setIntendedVersionId(QString version) +{ + settings()->set("IntendedJarVersion", version); + setShouldUpdate(true); + return true; +} + +bool LegacyInstance::shouldUpdate() const +{ + QVariant var = settings()->get("ShouldUpdate"); + if (!var.isValid() || var.toBool() == false) + { + return intendedVersionId() != currentVersionId(); + } + return true; +} + +void LegacyInstance::setShouldUpdate(bool val) +{ + settings()->set("ShouldUpdate", val); +} + +QString LegacyInstance::defaultBaseJar() const +{ + return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar"; +} + +QString LegacyInstance::defaultCustomBaseJar() const +{ + return FS::PathCombine(binDir(), "mcbackup.jar"); +} + +QString LegacyInstance::lwjglFolder() const +{ + return m_lwjglFolderSetting->get().toString(); +} + +QString LegacyInstance::typeName() const +{ + return tr("Legacy"); +} diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h new file mode 100644 index 00000000..3bef240d --- /dev/null +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -0,0 +1,142 @@ +/* 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/MinecraftInstance.h" + +#include "multimc_logic_export.h" + +class ModList; +class Task; + +class MULTIMC_LOGIC_EXPORT LegacyInstance : public MinecraftInstance +{ + Q_OBJECT +public: + + explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + + virtual void init() override {}; + + /// Path to the instance's minecraft.jar + QString runnableJar() const; + + //! Path to the instance's modlist file. + QString modListFile() const; + + /* + ////// Edit Instance Dialog stuff ////// + virtual QList<BasePage *> getPages(); + virtual QString dialogTitle(); + */ + + ////// Mod Lists ////// + std::shared_ptr<ModList> jarModList() const ; + virtual QList< Mod > getJarMods() const override; + std::shared_ptr<ModList> coreModList() const; + std::shared_ptr<ModList> loaderModList() const; + std::shared_ptr<ModList> texturePackList() const override; + std::shared_ptr<WorldList> worldList() const override; + + ////// Directories ////// + QString libDir() const; + QString savesDir() const; + QString texturePacksDir() const; + QString jarModsDir() const; + QString binDir() const; + QString loaderModsDir() const; + QString coreModsDir() const; + QString resourceDir() const; + virtual QString instanceConfigFolder() const override; + + /// Get the curent base jar of this instance. By default, it's the + /// versions/$version/$version.jar + QString baseJar() const; + + /// the default base jar of this instance + QString defaultBaseJar() const; + /// the default custom base jar of this instance + QString defaultCustomBaseJar() const; + + /*! + * Whether or not custom base jar is used + */ + bool shouldUseCustomBaseJar() const; + void setShouldUseCustomBaseJar(bool val); + + /*! + * The value of the custom base jar + */ + QString customBaseJar() const; + void setCustomBaseJar(QString val); + + /*! + * Whether or not the instance's minecraft.jar needs to be rebuilt. + * If this is true, when the instance launches, its jar mods will be + * re-added to a fresh minecraft.jar file. + */ + bool shouldRebuild() const; + void setShouldRebuild(bool val); + + virtual QString currentVersionId() const override; + + //! The version of LWJGL that this instance uses. + QString lwjglVersion() const; + + //! Where the lwjgl versions foor this instance can be found... HACK HACK HACK + QString lwjglFolder() const; + + /// st the version of LWJGL libs this instance will use + void setLWJGLVersion(QString val); + + virtual QString intendedVersionId() const override; + virtual bool setIntendedVersionId(QString version) override; + + virtual QSet<QString> traits() override + { + return {"legacy-instance", "texturepacks"}; + }; + + virtual bool shouldUpdate() const override; + virtual void setShouldUpdate(bool val) override; + virtual std::shared_ptr<Task> createUpdateTask() override; + + virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override; + + virtual std::shared_ptr<Task> createJarModdingTask() override; + + virtual QString createLaunchScript(AuthSessionPtr session) override; + + virtual void cleanupAfterRun() override; + + virtual QString typeName() const override; + + bool canExport() const override + { + return true; + } + +protected: + mutable std::shared_ptr<ModList> jar_mod_list; + mutable std::shared_ptr<ModList> core_mod_list; + mutable std::shared_ptr<ModList> loader_mod_list; + mutable std::shared_ptr<ModList> texture_pack_list; + mutable std::shared_ptr<WorldList> m_world_list; + std::shared_ptr<Setting> m_lwjglFolderSetting; +protected +slots: + virtual void jarModsChanged(); +}; diff --git a/api/logic/minecraft/legacy/LegacyUpdate.cpp b/api/logic/minecraft/legacy/LegacyUpdate.cpp new file mode 100644 index 00000000..2d7e8dd2 --- /dev/null +++ b/api/logic/minecraft/legacy/LegacyUpdate.cpp @@ -0,0 +1,393 @@ +/* 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 <QStringList> +#include <quazip.h> +#include <quazipfile.h> +#include <QDebug> + +#include "Env.h" +#include "BaseInstance.h" +#include "net/URLConstants.h" +#include "MMCZip.h" + +#include "LegacyUpdate.h" + +#include "LwjglVersionList.h" +#include "minecraft/MinecraftVersionList.h" +#include "minecraft/ModList.h" +#include "LegacyInstance.h" +#include <FileSystem.h> + +LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void LegacyUpdate::executeTask() +{ + fmllibsStart(); +} + +void LegacyUpdate::fmllibsStart() +{ + // Get the mod list + LegacyInstance *inst = (LegacyInstance *)m_inst; + auto modList = inst->jarModList(); + + bool forge_present = false; + + QString version = inst->intendedVersionId(); + auto & fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + lwjglStart(); + return; + } + + auto &libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + for (unsigned i = 0; i < modList->size(); i++) + { + auto &mod = modList->operator[](i); + + // do not use disabled mods. + if (!mod.enabled()) + continue; + + if (mod.type() != Mod::MOD_ZIPFILE) + continue; + + if (mod.mmc_id().contains("forge", Qt::CaseInsensitive)) + { + forge_present = true; + break; + } + if (mod.mmc_id().contains("fml", Qt::CaseInsensitive)) + { + forge_present = true; + break; + } + } + // we don't... + if (!forge_present) + { + lwjglStart(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto &lib : libList) + { + QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + lwjglStart(); + return; + } + + // download missing libs to our place + setStatus(tr("Dowloading FML libraries...")); + auto dljob = new NetJob("FML libraries"); + auto metacache = ENV.metacache(); + for (auto &lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename + : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; + dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry)); + } + + connect(dljob, &NetJob::succeeded, this, &LegacyUpdate::fmllibsFinished); + connect(dljob, &NetJob::failed, this, &LegacyUpdate::fmllibsFailed); + connect(dljob, &NetJob::progress, this, &LegacyUpdate::progress); + legacyDownloadJob.reset(dljob); + legacyDownloadJob->start(); +} + +void LegacyUpdate::fmllibsFinished() +{ + legacyDownloadJob.reset(); + if(!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + LegacyInstance *inst = (LegacyInstance *)m_inst; + auto metacache = ENV.metacache(); + int index = 0; + for (auto &lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = FS::PathCombine(inst->libDir(), lib.filename); + if(!FS::ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + lwjglStart(); +} + +void LegacyUpdate::fmllibsFailed(QString reason) +{ + emitFailed(tr("Game update failed: it was impossible to fetch the required FML libraries. Reason: %1").arg(reason)); + return; +} + +void LegacyUpdate::lwjglStart() +{ + LegacyInstance *inst = (LegacyInstance *)m_inst; + + lwjglVersion = inst->lwjglVersion(); + lwjglTargetPath = FS::PathCombine(inst->lwjglFolder(), lwjglVersion); + lwjglNativesPath = FS::PathCombine(lwjglTargetPath, "natives"); + + // if the 'done' file exists, we don't have to download this again + QFileInfo doneFile(FS::PathCombine(lwjglTargetPath, "done")); + if (doneFile.exists()) + { + jarStart(); + return; + } + + auto list = std::dynamic_pointer_cast<LWJGLVersionList>(ENV.getVersionList("org.lwjgl.legacy")); + if (!list->isLoaded()) + { + emitFailed("Too soon! Let the LWJGL list load :)"); + return; + } + + setStatus(tr("Downloading new LWJGL...")); + auto version = std::dynamic_pointer_cast<LWJGLVersion>(list->findVersion(lwjglVersion)); + if (!version) + { + emitFailed("Game update failed: the selected LWJGL version is invalid."); + return; + } + + QString url = version->url(); + QUrl realUrl(url); + QString hostname = realUrl.host(); + auto worker = ENV.qnam(); + QNetworkRequest req(realUrl); + req.setRawHeader("Host", hostname.toLatin1()); + req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); + QNetworkReply *rep = worker->get(req); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, &QNetworkReply::downloadProgress, this, &LegacyUpdate::progress); + connect(worker.get(), &QNetworkAccessManager::finished, this, &LegacyUpdate::lwjglFinished); +} + +void LegacyUpdate::lwjglFinished(QNetworkReply *reply) +{ + if (m_reply.get() != reply) + { + return; + } + if (reply->error() != QNetworkReply::NoError) + { + emitFailed("Failed to download: " + reply->errorString() + + "\nSometimes you have to wait a bit if you download many LWJGL versions in " + "a row. YMMV"); + return; + } + auto worker = ENV.qnam(); + // Here i check if there is a cookie for me in the reply and extract it + QList<QNetworkCookie> cookies = + qvariant_cast<QList<QNetworkCookie>>(reply->header(QNetworkRequest::SetCookieHeader)); + if (cookies.count() != 0) + { + // you must tell which cookie goes with which url + worker->cookieJar()->setCookiesFromUrl(cookies, QUrl("sourceforge.net")); + } + + // here you can check for the 302 or whatever other header i need + QVariant newLoc = reply->header(QNetworkRequest::LocationHeader); + if (newLoc.isValid()) + { + QString redirectedTo = reply->header(QNetworkRequest::LocationHeader).toString(); + QUrl realUrl(redirectedTo); + QString hostname = realUrl.host(); + QNetworkRequest req(redirectedTo); + req.setRawHeader("Host", hostname.toLatin1()); + req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); + QNetworkReply *rep = worker->get(req); + connect(rep, &QNetworkReply::downloadProgress, this, &LegacyUpdate::progress); + m_reply = std::shared_ptr<QNetworkReply>(rep); + return; + } + QFile saveMe("lwjgl.zip"); + saveMe.open(QIODevice::WriteOnly); + saveMe.write(m_reply->readAll()); + saveMe.close(); + setStatus(tr("Installing new LWJGL...")); + extractLwjgl(); + jarStart(); +} +void LegacyUpdate::extractLwjgl() +{ + // make sure the directories are there + + bool success = FS::ensureFolderPathExists(lwjglNativesPath); + + if (!success) + { + emitFailed("Failed to extract the lwjgl libs - error when creating required folders."); + return; + } + + QuaZip zip("lwjgl.zip"); + if (!zip.open(QuaZip::mdUnzip)) + { + emitFailed("Failed to extract the lwjgl libs - not a valid archive."); + return; + } + + // and now we are going to access files inside it + QuaZipFile file(&zip); + const QString jarNames[] = {"jinput.jar", "lwjgl_util.jar", "lwjgl.jar"}; + for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile()) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + emitFailed("Failed to extract the lwjgl libs - error while reading archive."); + return; + } + QuaZipFileInfo info; + QString name = file.getActualFileName(); + if (name.endsWith('/')) + { + file.close(); + continue; + } + QString destFileName; + // Look for the jars + for (int i = 0; i < 3; i++) + { + if (name.endsWith(jarNames[i])) + { + destFileName = FS::PathCombine(lwjglTargetPath, jarNames[i]); + } + } + // Not found? look for the natives + if (destFileName.isEmpty()) + { +#ifdef Q_OS_WIN32 + QString nativesDir = "windows"; +#else +#ifdef Q_OS_MAC + QString nativesDir = "macosx"; +#else + QString nativesDir = "linux"; +#endif +#endif + if (name.contains(nativesDir)) + { + int lastSlash = name.lastIndexOf('/'); + int lastBackSlash = name.lastIndexOf('\\'); + if (lastSlash != -1) + name = name.mid(lastSlash + 1); + else if (lastBackSlash != -1) + name = name.mid(lastBackSlash + 1); + destFileName = FS::PathCombine(lwjglNativesPath, name); + } + } + // Now if destFileName is still empty, go to the next file. + if (!destFileName.isEmpty()) + { + setStatus(tr("Installing new LWJGL - extracting ") + name + "..."); + QFile output(destFileName); + output.open(QIODevice::WriteOnly); + output.write(file.readAll()); + output.close(); + } + file.close(); // do not forget to close! + } + zip.close(); + m_reply.reset(); + QFile doneFile(FS::PathCombine(lwjglTargetPath, "done")); + doneFile.open(QIODevice::WriteOnly); + doneFile.write("done."); + doneFile.close(); +} + +void LegacyUpdate::lwjglFailed(QString reason) +{ + emitFailed(tr("Bad stuff happened while trying to get the lwjgl libs: %1").arg(reason)); +} + +void LegacyUpdate::jarStart() +{ + LegacyInstance *inst = (LegacyInstance *)m_inst; + if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar()) + { + emitSucceeded(); + return; + } + + setStatus(tr("Checking for jar updates...")); + // Make directories + QDir binDir(inst->binDir()); + if (!binDir.exists() && !binDir.mkpath(".")) + { + emitFailed("Failed to create bin folder."); + return; + } + + // Build a list of URLs that will need to be downloaded. + setStatus(tr("Downloading new minecraft.jar ...")); + + QString version_id = inst->intendedVersionId(); + + auto dljob = new NetJob("Minecraft.jar for version " + version_id); + + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("versions", URLConstants::getJarPath(version_id)); + dljob->addNetAction(CacheDownload::make(QUrl(URLConstants::getLegacyJarUrl(version_id)), entry)); + connect(dljob, SIGNAL(succeeded()), SLOT(jarFinished())); + connect(dljob, SIGNAL(failed(QString)), SLOT(jarFailed(QString))); + connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + legacyDownloadJob.reset(dljob); + legacyDownloadJob->start(); +} + +void LegacyUpdate::jarFinished() +{ + // process the jar + emitSucceeded(); +} + +void LegacyUpdate::jarFailed(QString reason) +{ + // bad, bad + emitFailed(tr("Failed to download the minecraft jar: %1.").arg(reason)); +} diff --git a/api/logic/minecraft/legacy/LegacyUpdate.h b/api/logic/minecraft/legacy/LegacyUpdate.h new file mode 100644 index 00000000..c52bf934 --- /dev/null +++ b/api/logic/minecraft/legacy/LegacyUpdate.h @@ -0,0 +1,70 @@ +/* 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 <QObject> +#include <QList> +#include <QUrl> + +#include "net/NetJob.h" +#include "tasks/Task.h" +#include "minecraft/VersionFilterData.h" + +class MinecraftVersion; +class BaseInstance; +class QuaZip; +class Mod; + +class LegacyUpdate : public Task +{ + Q_OBJECT +public: + explicit LegacyUpdate(BaseInstance *inst, QObject *parent = 0); + virtual void executeTask(); + +private +slots: + void lwjglStart(); + void lwjglFinished(QNetworkReply *); + void lwjglFailed(QString reason); + + void jarStart(); + void jarFinished(); + void jarFailed(QString reason); + + void fmllibsStart(); + void fmllibsFinished(); + void fmllibsFailed(QString reason); + + void extractLwjgl(); + +private: + + std::shared_ptr<QNetworkReply> m_reply; + + // target version, determined during this task + // MinecraftVersion *targetVersion; + QString lwjglURL; + QString lwjglVersion; + + QString lwjglTargetPath; + QString lwjglNativesPath; + +private: + NetJobPtr legacyDownloadJob; + BaseInstance *m_inst = nullptr; + QList<FMLlib> fmlLibsToProcess; +}; diff --git a/api/logic/minecraft/legacy/LwjglVersionList.cpp b/api/logic/minecraft/legacy/LwjglVersionList.cpp new file mode 100644 index 00000000..bb017368 --- /dev/null +++ b/api/logic/minecraft/legacy/LwjglVersionList.cpp @@ -0,0 +1,189 @@ +/* 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 "LwjglVersionList.h" +#include "Env.h" + +#include <QtNetwork> +#include <QtXml> +#include <QRegExp> + +#include <QDebug> + +#define RSS_URL "http://sourceforge.net/projects/java-game-lib/rss" + +LWJGLVersionList::LWJGLVersionList(QObject *parent) : BaseVersionList(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 = m_vlist.at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + return version->name(); + + case Qt::ToolTipRole: + return version->url(); + + default: + return QVariant(); + } +} + +QVariant LWJGLVersionList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + return tr("Version"); + + case Qt::ToolTipRole: + return tr("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); + auto worker = ENV.qnam(); + QNetworkRequest req(QUrl(RSS_URL)); + req.setRawHeader("Accept", "application/rss+xml, text/xml, */*"); + req.setRawHeader("User-Agent", "MultiMC/5.0 (Uncached)"); + reply = worker->get(req); + 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; + auto rawData = reply->readAll(); + if (!doc.setContent(rawData, false, &xmlErrorMsg, &errorLine)) + { + failed("Failed to load LWJGL list. XML error: " + xmlErrorMsg + " at line " + + QString::number(errorLine)); + setLoading(false); + return; + } + + QDomNodeList items = doc.elementsByTagName("item"); + + QList<PtrLWJGLVersion> tempList; + + for (int i = 0; i < items.length(); i++) + { + Q_ASSERT_X(items.at(i).isElement(), "load LWJGL list", + "XML element isn't an element... wat?"); + + QDomElement linkElement = getDomElementByTagName(items.at(i).toElement(), "link"); + if (linkElement.isNull()) + { + qDebug() << "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) + 6); + // Subtract 4 here to remove the .zip file extension. + name = name.left(lwjglRegex.matchedLength() - 10); + + QUrl url(link); + if (!url.isValid()) + { + qWarning() << "LWJGL version URL isn't valid:" << link << "Skipping."; + continue; + } + qDebug() << "Discovered LWGL version" << name << "at" << link; + tempList.append(std::make_shared<LWJGLVersion>(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(); +} + +void LWJGLVersionList::failed(QString msg) +{ + qCritical() << msg; + emit loadListFailed(msg); +} + +void LWJGLVersionList::finished() +{ + emit loadListFinished(); +} + +void LWJGLVersionList::setLoading(bool loading) +{ + m_loading = loading; + emit loadingStateUpdated(m_loading); +} diff --git a/api/logic/minecraft/legacy/LwjglVersionList.h b/api/logic/minecraft/legacy/LwjglVersionList.h new file mode 100644 index 00000000..f043f6e2 --- /dev/null +++ b/api/logic/minecraft/legacy/LwjglVersionList.h @@ -0,0 +1,156 @@ +/* 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 <QObject> +#include <QAbstractListModel> +#include <QUrl> +#include <QNetworkReply> +#include <memory> + +#include "BaseVersion.h" +#include "BaseVersionList.h" + +#include "multimc_logic_export.h" + +class LWJGLVersion; +typedef std::shared_ptr<LWJGLVersion> PtrLWJGLVersion; + +class MULTIMC_LOGIC_EXPORT LWJGLVersion : public BaseVersion +{ +public: + LWJGLVersion(const QString &name, const QString &url) + : m_name(name), m_url(url) + { + } + + virtual QString descriptor() + { + return m_name; + } + + virtual QString name() + { + return m_name; + } + + virtual QString typeString() const + { + return QObject::tr("Upstream"); + } + + QString url() const + { + return m_url; + } + +protected: + QString m_name; + QString m_url; +}; + +class MULTIMC_LOGIC_EXPORT LWJGLVersionList : public BaseVersionList +{ + Q_OBJECT +public: + explicit LWJGLVersionList(QObject *parent = 0); + + bool isLoaded() override + { + return m_vlist.length() > 0; + } + virtual const BaseVersionPtr at(int i) const override + { + return m_vlist[i]; + } + + virtual Task* getLoadTask() override + { + return nullptr; + } + + virtual void sortVersions() override {}; + + virtual void updateListData(QList< BaseVersionPtr > versions) override {}; + + int count() const override + { + return m_vlist.length(); + } + + virtual QVariant data(const QModelIndex &index, int role) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent) const override + { + return count(); + } + virtual int columnCount(const QModelIndex &parent) const override; + + virtual bool isLoading() const; + virtual bool errored() const + { + return m_errored; + } + + virtual QString lastErrorMsg() const + { + return m_lastErrorMsg; + } + +public +slots: + /*! + * Loads the version list. + * This is done asynchronously. On success, the loadListFinished() signal will + * be emitted. The list model will be reset as well, resulting in the modelReset() + * signal being emitted. Note that the model will be reset before loadListFinished() is + * emitted. + * If loading the list failed, the loadListFailed(QString msg), + * signal will be emitted. + */ + virtual void loadList(); + +signals: + /*! + * Emitted when the list either starts or finishes loading. + * \param loading Whether or not the list is loading. + */ + void loadingStateUpdated(bool loading); + + void loadListFinished(); + + void loadListFailed(QString msg); + +private: + QList<PtrLWJGLVersion> m_vlist; + + QNetworkReply *m_netReply; + 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/api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp b/api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp new file mode 100644 index 00000000..25297fa4 --- /dev/null +++ b/api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp @@ -0,0 +1,142 @@ +/* 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 "LiteLoaderInstaller.h" + +#include <QJsonArray> +#include <QJsonDocument> + +#include <QDebug> + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/Library.h" +#include "minecraft/onesix/OneSixInstance.h" +#include <minecraft/onesix/OneSixVersionFormat.h> +#include "minecraft/liteloader/LiteLoaderVersionList.h" +#include "Exception.h" + +LiteLoaderInstaller::LiteLoaderInstaller() : BaseInstaller() +{ +} + +void LiteLoaderInstaller::prepare(LiteLoaderVersionPtr version) +{ + m_version = version; +} +bool LiteLoaderInstaller::add(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + + QJsonObject obj; + + obj.insert("mainClass", QString("net.minecraft.launchwrapper.Launch")); + obj.insert("+tweakers", QJsonArray::fromStringList(QStringList() << m_version->tweakClass)); + obj.insert("order", 10); + + QJsonArray libraries; + + for (auto Library : m_version->libraries) + { + libraries.append(OneSixVersionFormat::libraryToJson(Library.get())); + } + + // liteloader + { + Library liteloaderLib("com.mumfrey:liteloader:" + m_version->version); + liteloaderLib.setAbsoluteUrl(QString("http://dl.liteloader.com/versions/com/mumfrey/liteloader/%1/%2").arg(m_version->mcVersion, m_version->file)); + QJsonObject llLibObj = OneSixVersionFormat::libraryToJson(&liteloaderLib); + libraries.append(llLibObj); + } + + obj.insert("+libraries", libraries); + obj.insert("name", QString("LiteLoader")); + obj.insert("fileId", id()); + obj.insert("version", m_version->version); + 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; +} + +class LiteLoaderInstallTask : public Task +{ + Q_OBJECT +public: + LiteLoaderInstallTask(LiteLoaderInstaller *installer, OneSixInstance *instance, + BaseVersionPtr version, QObject *parent) + : Task(parent), m_installer(installer), m_instance(instance), m_version(version) + { + } + +protected: + void executeTask() override + { + LiteLoaderVersionPtr liteloaderVersion = + std::dynamic_pointer_cast<LiteLoaderVersion>(m_version); + if (!liteloaderVersion) + { + return; + } + m_installer->prepare(liteloaderVersion); + if (!m_installer->add(m_instance)) + { + emitFailed(tr("For reasons unknown, the LiteLoader installation failed. Check your " + "MultiMC log files for details.")); + } + else + { + 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: + LiteLoaderInstaller *m_installer; + OneSixInstance *m_instance; + BaseVersionPtr m_version; +}; + +Task *LiteLoaderInstaller::createInstallTask(OneSixInstance *instance, + BaseVersionPtr version, + QObject *parent) +{ + return new LiteLoaderInstallTask(this, instance, version, parent); +} + +#include "LiteLoaderInstaller.moc" diff --git a/api/logic/minecraft/liteloader/LiteLoaderInstaller.h b/api/logic/minecraft/liteloader/LiteLoaderInstaller.h new file mode 100644 index 00000000..fe0aee3d --- /dev/null +++ b/api/logic/minecraft/liteloader/LiteLoaderInstaller.h @@ -0,0 +1,39 @@ +/* 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 <QString> +#include <QMap> + +#include "BaseInstaller.h" +#include "LiteLoaderVersionList.h" + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT LiteLoaderInstaller : public BaseInstaller +{ +public: + LiteLoaderInstaller(); + + void prepare(LiteLoaderVersionPtr version); + bool add(OneSixInstance *to) override; + virtual QString id() const override { return "com.mumfrey.liteloader"; } + + Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override; + +private: + LiteLoaderVersionPtr m_version; +}; diff --git a/api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp b/api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp new file mode 100644 index 00000000..b0c9736a --- /dev/null +++ b/api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp @@ -0,0 +1,276 @@ +/* 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 "LiteLoaderVersionList.h" +#include <minecraft/onesix/OneSixVersionFormat.h> +#include "Env.h" +#include "net/URLConstants.h" +#include "Exception.h" + +#include <QtXml> + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <QJsonParseError> + +#include <QtAlgorithms> + +#include <QtNetwork> + +LiteLoaderVersionList::LiteLoaderVersionList(QObject *parent) : BaseVersionList(parent) +{ +} + +Task *LiteLoaderVersionList::getLoadTask() +{ + return new LLListLoadTask(this); +} + +bool LiteLoaderVersionList::isLoaded() +{ + return m_loaded; +} + +const BaseVersionPtr LiteLoaderVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int LiteLoaderVersionList::count() const +{ + return m_vlist.count(); +} + +static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second) +{ + auto left = std::dynamic_pointer_cast<LiteLoaderVersion>(first); + auto right = std::dynamic_pointer_cast<LiteLoaderVersion>(second); + return left->timestamp > right->timestamp; +} + +void LiteLoaderVersionList::sortVersions() +{ + beginResetModel(); + std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions); + endResetModel(); +} + +QVariant LiteLoaderVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast<LiteLoaderVersion>(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->mcVersion; + + case RecommendedRole: + return version->isLatest; + + default: + return QVariant(); + } +} + +QList<BaseVersionList::ModelRoles> LiteLoaderVersionList::providesRoles() +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole}; +} + +BaseVersionPtr LiteLoaderVersionList::getLatestStable() const +{ + for (int i = 0; i < m_vlist.length(); i++) + { + auto ver = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist.at(i)); + if (ver->isLatest) + { + return m_vlist.at(i); + } + } + return BaseVersionPtr(); +} + +void LiteLoaderVersionList::updateListData(QList<BaseVersionPtr> versions) +{ + beginResetModel(); + m_vlist = versions; + m_loaded = true; + std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions); + endResetModel(); +} + +LLListLoadTask::LLListLoadTask(LiteLoaderVersionList *vlist) +{ + m_list = vlist; +} + +LLListLoadTask::~LLListLoadTask() +{ +} + +void LLListLoadTask::executeTask() +{ + setStatus(tr("Loading LiteLoader version list...")); + auto job = new NetJob("Version index"); + // we do not care if the version is stale or not. + auto liteloaderEntry = ENV.metacache()->resolveEntry("liteloader", "versions.json"); + + // verify by poking the server. + liteloaderEntry->setStale(true); + + job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::LITELOADER_URL), + liteloaderEntry)); + + connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed())); + + listJob.reset(job); + connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded())); + connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + listJob->start(); +} + +void LLListLoadTask::listFailed() +{ + emitFailed("Failed to load LiteLoader version list."); + return; +} + +void LLListLoadTask::listDownloaded() +{ + QByteArray data; + { + auto dlJob = listDownload; + auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + emitFailed("Failed to open the LiteLoader version list."); + return; + } + data = listFile.readAll(); + listFile.close(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing version list JSON:" + jsonError.errorString()); + return; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing version list JSON: jsonDoc is not an object"); + return; + } + + const QJsonObject root = jsonDoc.object(); + + // Now, get the array of versions. + if (!root.value("versions").isObject()) + { + emitFailed("Error parsing version list JSON: missing 'versions' object"); + return; + } + + auto meta = root.value("meta").toObject(); + QString description = meta.value("description").toString(tr("This is a lightweight loader for mods that don't change game mechanics.")); + QString defaultUrl = meta.value("url").toString("http://dl.liteloader.com"); + QString authors = meta.value("authors").toString("Mumfrey"); + auto versions = root.value("versions").toObject(); + + QList<BaseVersionPtr> tempList; + for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt) + { + const QString mcVersion = vIt.key(); + QString latest; + const QJsonObject artefacts = vIt.value() + .toObject() + .value("artefacts") + .toObject() + .value("com.mumfrey:liteloader") + .toObject(); + QList<BaseVersionPtr> perMcVersionList; + for (auto aIt = artefacts.begin(); aIt != artefacts.end(); ++aIt) + { + const QString identifier = aIt.key(); + const QJsonObject artefact = aIt.value().toObject(); + if (identifier == "latest") + { + latest = artefact.value("version").toString(); + continue; + } + LiteLoaderVersionPtr version(new LiteLoaderVersion()); + version->version = artefact.value("version").toString(); + version->file = artefact.value("file").toString(); + version->mcVersion = mcVersion; + version->md5 = artefact.value("md5").toString(); + version->timestamp = artefact.value("timestamp").toString().toInt(); + version->tweakClass = artefact.value("tweakClass").toString(); + version->authors = authors; + version->description = description; + version->defaultUrl = defaultUrl; + const QJsonArray libs = artefact.value("libraries").toArray(); + for (auto lIt = libs.begin(); lIt != libs.end(); ++lIt) + { + auto libobject = (*lIt).toObject(); + try + { + auto lib = OneSixVersionFormat::libraryFromJson(libobject, "versions.json"); + // hack to make liteloader 1.7.10_00 work + if(lib->rawName() == GradleSpecifier("org.ow2.asm:asm-all:5.0.3")) + { + lib->setRepositoryURL("http://repo.maven.apache.org/maven2/"); + } + version->libraries.append(lib); + } + catch (Exception &e) + { + qCritical() << "Couldn't read JSON object:"; + continue; + } + } + perMcVersionList.append(version); + } + for (auto version : perMcVersionList) + { + auto v = std::dynamic_pointer_cast<LiteLoaderVersion>(version); + v->isLatest = v->version == latest; + } + tempList.append(perMcVersionList); + } + m_list->updateListData(tempList); + + emitSucceeded(); +} diff --git a/api/logic/minecraft/liteloader/LiteLoaderVersionList.h b/api/logic/minecraft/liteloader/LiteLoaderVersionList.h new file mode 100644 index 00000000..1dba4b6a --- /dev/null +++ b/api/logic/minecraft/liteloader/LiteLoaderVersionList.h @@ -0,0 +1,119 @@ +/* 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 <QObject> + +#include <QString> +#include <QStringList> +#include "BaseVersion.h" +#include "BaseVersionList.h" +#include "tasks/Task.h" +#include "net/NetJob.h" +#include <minecraft/Library.h> + +#include "multimc_logic_export.h" + +class LLListLoadTask; +class QNetworkReply; + +class LiteLoaderVersion : public BaseVersion +{ +public: + QString descriptor() override + { + if (isLatest) + { + return QObject::tr("Latest"); + } + return QString(); + } + QString typeString() const override + { + return mcVersion; + } + QString name() override + { + return version; + } + + // important info + QString version; + QString file; + QString mcVersion; + QString md5; + int timestamp; + bool isLatest; + QString tweakClass; + QList<LibraryPtr> libraries; + + // meta + QString defaultUrl; + QString description; + QString authors; +}; +typedef std::shared_ptr<LiteLoaderVersion> LiteLoaderVersionPtr; + +class MULTIMC_LOGIC_EXPORT LiteLoaderVersionList : public BaseVersionList +{ + Q_OBJECT +public: + friend class LLListLoadTask; + + explicit LiteLoaderVersionList(QObject *parent = 0); + + virtual Task *getLoadTask(); + virtual bool isLoaded(); + virtual const BaseVersionPtr at(int i) const; + virtual int count() const; + virtual void sortVersions(); + virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const; + virtual QList< ModelRoles > providesRoles(); + + virtual BaseVersionPtr getLatestStable() const; + +protected: + QList<BaseVersionPtr> m_vlist; + + bool m_loaded = false; + +protected +slots: + virtual void updateListData(QList<BaseVersionPtr> versions); +}; + +class LLListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit LLListLoadTask(LiteLoaderVersionList *vlist); + ~LLListLoadTask(); + + virtual void executeTask(); + +protected +slots: + void listDownloaded(); + void listFailed(); + +protected: + NetJobPtr listJob; + CacheDownloadPtr listDownload; + LiteLoaderVersionList *m_list; +}; + +Q_DECLARE_METATYPE(LiteLoaderVersionPtr) diff --git a/api/logic/minecraft/onesix/OneSixInstance.cpp b/api/logic/minecraft/onesix/OneSixInstance.cpp new file mode 100644 index 00000000..258e26c5 --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixInstance.cpp @@ -0,0 +1,597 @@ +/* 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 <QDebug> +#include <Env.h> + +#include "OneSixInstance.h" +#include "OneSixUpdate.h" +#include "OneSixProfileStrategy.h" + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/VersionBuildError.h" +#include "launch/LaunchTask.h" +#include "launch/steps/PreLaunchCommand.h" +#include "launch/steps/Update.h" +#include "launch/steps/LaunchMinecraft.h" +#include "launch/steps/PostLaunchCommand.h" +#include "launch/steps/TextPrint.h" +#include "launch/steps/ModMinecraftJar.h" +#include "launch/steps/CheckJava.h" +#include "MMCZip.h" + +#include "minecraft/AssetsUtils.h" +#include "minecraft/WorldList.h" +#include <FileSystem.h> + +OneSixInstance::OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : MinecraftInstance(globalSettings, settings, rootDir) +{ + m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); +} + +void OneSixInstance::init() +{ + createProfile(); +} + +void OneSixInstance::createProfile() +{ + m_profile.reset(new MinecraftProfile(new OneSixProfileStrategy(this))); +} + +QSet<QString> OneSixInstance::traits() +{ + auto version = getMinecraftProfile(); + if (!version) + { + return {"version-incomplete"}; + } + else + { + return version->getTraits(); + } +} + +std::shared_ptr<Task> OneSixInstance::createUpdateTask() +{ + return std::shared_ptr<Task>(new OneSixUpdate(this)); +} + +QString replaceTokensIn(QString text, QMap<QString, QString> with) +{ + QString result; + QRegExp token_regexp("\\$\\{(.+)\\}"); + token_regexp.setMinimal(true); + QStringList list; + int tail = 0; + int head = 0; + while ((head = token_regexp.indexIn(text, head)) != -1) + { + result.append(text.mid(tail, head - tail)); + QString key = token_regexp.cap(1); + auto iter = with.find(key); + if (iter != with.end()) + { + result.append(*iter); + } + head += token_regexp.matchedLength(); + tail = head; + } + result.append(text.mid(tail)); + return result; +} + +QStringList OneSixInstance::processMinecraftArgs(AuthSessionPtr session) +{ + QString args_pattern = m_profile->getMinecraftArguments(); + for (auto tweaker : m_profile->getTweakers()) + { + args_pattern += " --tweakClass " + tweaker; + } + + QMap<QString, QString> token_mapping; + // yggdrasil! + token_mapping["auth_username"] = session->username; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; + + // blatant self-promotion. + token_mapping["profile_name"] = token_mapping["version_name"] = "MultiMC5"; + if(m_profile->isVanilla()) + { + token_mapping["version_type"] = m_profile->getMinecraftVersionType(); + } + else + { + token_mapping["version_type"] = "custom"; + } + + QString absRootDir = QDir(minecraftRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + auto assets = m_profile->getMinecraftAssets(); + token_mapping["game_assets"] = AssetsUtils::reconstructAssets(assets->id).absolutePath(); + + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; + + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = assets->id; + + QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); + for (int i = 0; i < parts.length(); i++) + { + parts[i] = replaceTokensIn(parts[i], token_mapping); + } + return parts; +} + +QString OneSixInstance::createLaunchScript(AuthSessionPtr session) +{ + QString launchScript; + + if (!m_profile) + return nullptr; + + for(auto & mod: loaderModList()->allMods()) + { + if(!mod.enabled()) + continue; + if(mod.type() == Mod::MOD_FOLDER) + continue; + // TODO: proper implementation would need to descend into folders. + + launchScript += "mod " + mod.filename().completeBaseName() + "\n";; + } + + for(auto & coremod: coreModList()->allMods()) + { + if(!coremod.enabled()) + continue; + if(coremod.type() == Mod::MOD_FOLDER) + continue; + // TODO: proper implementation would need to descend into folders. + + launchScript += "coremod " + coremod.filename().completeBaseName() + "\n";; + } + + for(auto & jarmod: m_profile->getJarMods()) + { + launchScript += "jarmod " + jarmod->originalName + " (" + jarmod->name + ")\n"; + } + + auto mainClass = m_profile->getMainClass(); + if (!mainClass.isEmpty()) + { + launchScript += "mainClass " + mainClass + "\n"; + } + auto appletClass = m_profile->getAppletClass(); + if (!appletClass.isEmpty()) + { + launchScript += "appletClass " + appletClass + "\n"; + } + + // generic minecraft params + for (auto param : processMinecraftArgs(session)) + { + launchScript += "param " + param + "\n"; + } + + // window size, title and state, legacy + { + QString windowParams; + if (settings()->get("LaunchMaximized").toBool()) + windowParams = "max"; + else + windowParams = QString("%1x%2") + .arg(settings()->get("MinecraftWinWidth").toInt()) + .arg(settings()->get("MinecraftWinHeight").toInt()); + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + } + + // legacy auth + { + launchScript += "userName " + session->player_name + "\n"; + launchScript += "sessionId " + session->session + "\n"; + } + + // libraries and class path. + { + auto libs = m_profile->getLibraries(); + + QStringList jar, native, native32, native64; + for (auto lib : libs) + { + lib->getApplicableFiles(currentSystem, jar, native, native32, native64); + } + for(auto file: jar) + { + launchScript += "cp " + file + "\n"; + } + for(auto file: native) + { + launchScript += "ext " + file + "\n"; + } + for(auto file: native32) + { + launchScript += "ext32 " + file + "\n"; + } + for(auto file: native64) + { + launchScript += "ext64 " + file + "\n"; + } + QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); + launchScript += "natives " + natives_dir.absolutePath() + "\n"; + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + launchScript += "cp " + QDir(instanceRoot()).absoluteFilePath("minecraft.jar") + "\n"; + } + else + { + QString relpath = m_profile->getMinecraftVersion() + "/" + m_profile->getMinecraftVersion() + ".jar"; + launchScript += "cp " + versionsPath().absoluteFilePath(relpath) + "\n"; + } + } + + // traits. including legacyLaunch and others ;) + for (auto trait : m_profile->getTraits()) + { + launchScript += "traits " + trait + "\n"; + } + launchScript += "launcher onesix\n"; + return launchScript; +} + +std::shared_ptr<LaunchTask> OneSixInstance::createLaunchTask(AuthSessionPtr session) +{ + auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr())); + auto pptr = process.get(); + + // print a header + { + process->appendStep(std::make_shared<TextPrint>(pptr, "Minecraft folder is:\n" + minecraftRoot() + "\n\n", MessageLevel::MultiMC)); + } + { + auto step = std::make_shared<CheckJava>(pptr); + process->appendStep(step); + } + // run pre-launch command if that's needed + if(getPreLaunchCommand().size()) + { + auto step = std::make_shared<PreLaunchCommand>(pptr); + step->setWorkingDirectory(minecraftRoot()); + process->appendStep(step); + } + // if we aren't in offline mode,. + if(session->status != AuthSession::PlayableOffline) + { + process->appendStep(std::make_shared<Update>(pptr)); + } + // if there are any jar mods + if(getJarMods().size()) + { + auto step = std::make_shared<ModMinecraftJar>(pptr); + process->appendStep(step); + } + // actually launch the game + { + auto step = std::make_shared<LaunchMinecraft>(pptr); + step->setWorkingDirectory(minecraftRoot()); + step->setAuthSession(session); + process->appendStep(step); + } + // run post-exit command if that's needed + if(getPostExitCommand().size()) + { + auto step = std::make_shared<PostLaunchCommand>(pptr); + step->setWorkingDirectory(minecraftRoot()); + process->appendStep(step); + } + if (session) + { + process->setCensorFilter(createCensorFilterFromSession(session)); + } + return process; +} + +std::shared_ptr<Task> OneSixInstance::createJarModdingTask() +{ + class JarModTask : public Task + { + public: + explicit JarModTask(std::shared_ptr<OneSixInstance> inst) : Task(nullptr), m_inst(inst) + { + } + virtual void executeTask() + { + auto profile = m_inst->getMinecraftProfile(); + // nuke obsolete stripped jar(s) if needed + QString version_id = profile->getMinecraftVersion(); + QString strippedPath = version_id + "/" + version_id + "-stripped.jar"; + QFile strippedJar(strippedPath); + if(strippedJar.exists()) + { + strippedJar.remove(); + } + auto tempJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("temp.jar"); + QFile tempJar(tempJarPath); + if(tempJar.exists()) + { + tempJar.remove(); + } + auto finalJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("minecraft.jar"); + QFile finalJar(finalJarPath); + if(finalJar.exists()) + { + if(!finalJar.remove()) + { + emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); + return; + } + } + + // create temporary modded jar, if needed + auto jarMods = m_inst->getJarMods(); + if(jarMods.size()) + { + auto sourceJarPath = m_inst->versionsPath().absoluteFilePath(version_id + "/" + version_id + ".jar"); + QString localPath = version_id + "/" + version_id + ".jar"; + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + QString fullJarPath = entry->getFullPath(); + if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + } + emitSucceeded(); + } + std::shared_ptr<OneSixInstance> m_inst; + }; + return std::make_shared<JarModTask>(std::dynamic_pointer_cast<OneSixInstance>(shared_from_this())); +} + +void OneSixInstance::cleanupAfterRun() +{ + QString target_dir = FS::PathCombine(instanceRoot(), "natives/"); + QDir dir(target_dir); + dir.removeRecursively(); +} + +std::shared_ptr<ModList> OneSixInstance::loaderModList() const +{ + if (!m_loader_mod_list) + { + m_loader_mod_list.reset(new ModList(loaderModsDir())); + } + m_loader_mod_list->update(); + return m_loader_mod_list; +} + +std::shared_ptr<ModList> OneSixInstance::coreModList() const +{ + if (!m_core_mod_list) + { + m_core_mod_list.reset(new ModList(coreModsDir())); + } + m_core_mod_list->update(); + return m_core_mod_list; +} + +std::shared_ptr<ModList> OneSixInstance::resourcePackList() const +{ + if (!m_resource_pack_list) + { + m_resource_pack_list.reset(new ModList(resourcePacksDir())); + } + m_resource_pack_list->update(); + return m_resource_pack_list; +} + +std::shared_ptr<ModList> OneSixInstance::texturePackList() const +{ + if (!m_texture_pack_list) + { + m_texture_pack_list.reset(new ModList(texturePacksDir())); + } + m_texture_pack_list->update(); + return m_texture_pack_list; +} + +std::shared_ptr<WorldList> OneSixInstance::worldList() const +{ + if (!m_world_list) + { + m_world_list.reset(new WorldList(worldDir())); + } + return m_world_list; +} + +bool OneSixInstance::setIntendedVersionId(QString version) +{ + settings()->set("IntendedVersion", version); + if(getMinecraftProfile()) + { + clearProfile(); + } + emit propertiesChanged(this); + return true; +} + +QList< Mod > OneSixInstance::getJarMods() const +{ + QList<Mod> mods; + for (auto jarmod : m_profile->getJarMods()) + { + QString filePath = jarmodsPath().absoluteFilePath(jarmod->name); + mods.push_back(Mod(QFileInfo(filePath))); + } + return mods; +} + + +QString OneSixInstance::intendedVersionId() const +{ + return settings()->get("IntendedVersion").toString(); +} + +void OneSixInstance::setShouldUpdate(bool) +{ +} + +bool OneSixInstance::shouldUpdate() const +{ + return true; +} + +QString OneSixInstance::currentVersionId() const +{ + return intendedVersionId(); +} + +void OneSixInstance::reloadProfile() +{ + m_profile->reload(); + auto severity = m_profile->getProblemSeverity(); + if(severity == ProblemSeverity::PROBLEM_ERROR) + { + setFlag(VersionBrokenFlag); + } + else + { + unsetFlag(VersionBrokenFlag); + } + emit versionReloaded(); +} + +void OneSixInstance::clearProfile() +{ + m_profile->clear(); + emit versionReloaded(); +} + +std::shared_ptr<MinecraftProfile> OneSixInstance::getMinecraftProfile() const +{ + return m_profile; +} + +QDir OneSixInstance::librariesPath() const +{ + return QDir::current().absoluteFilePath("libraries"); +} + +QDir OneSixInstance::jarmodsPath() const +{ + return QDir(jarModsDir()); +} + +QDir OneSixInstance::versionsPath() const +{ + return QDir::current().absoluteFilePath("versions"); +} + +bool OneSixInstance::providesVersionFile() const +{ + return false; +} + +bool OneSixInstance::reload() +{ + if (BaseInstance::reload()) + { + try + { + reloadProfile(); + return true; + } + catch (...) + { + return false; + } + } + return false; +} + +QString OneSixInstance::loaderModsDir() const +{ + return FS::PathCombine(minecraftRoot(), "mods"); +} + +QString OneSixInstance::coreModsDir() const +{ + return FS::PathCombine(minecraftRoot(), "coremods"); +} + +QString OneSixInstance::resourcePacksDir() const +{ + return FS::PathCombine(minecraftRoot(), "resourcepacks"); +} + +QString OneSixInstance::texturePacksDir() const +{ + return FS::PathCombine(minecraftRoot(), "texturepacks"); +} + +QString OneSixInstance::instanceConfigFolder() const +{ + return FS::PathCombine(minecraftRoot(), "config"); +} + +QString OneSixInstance::jarModsDir() const +{ + return FS::PathCombine(instanceRoot(), "jarmods"); +} + +QString OneSixInstance::libDir() const +{ + return FS::PathCombine(minecraftRoot(), "lib"); +} + +QString OneSixInstance::worldDir() const +{ + return FS::PathCombine(minecraftRoot(), "saves"); +} + +QStringList OneSixInstance::extraArguments() const +{ + auto list = BaseInstance::extraArguments(); + auto version = getMinecraftProfile(); + if (!version) + return list; + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true", + "-Dfml.ignorePatchDiscrepancies=true"}); + } + return list; +} + +std::shared_ptr<OneSixInstance> OneSixInstance::getSharedPtr() +{ + return std::dynamic_pointer_cast<OneSixInstance>(BaseInstance::getSharedPtr()); +} + +QString OneSixInstance::typeName() const +{ + return tr("OneSix"); +} diff --git a/api/logic/minecraft/onesix/OneSixInstance.h b/api/logic/minecraft/onesix/OneSixInstance.h new file mode 100644 index 00000000..2dfab48c --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixInstance.h @@ -0,0 +1,117 @@ +/* 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/MinecraftInstance.h" + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/ModList.h" + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT OneSixInstance : public MinecraftInstance +{ + Q_OBJECT +public: + explicit OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual ~OneSixInstance(){}; + + virtual void init() override; + + ////// Mod Lists ////// + std::shared_ptr<ModList> loaderModList() const; + std::shared_ptr<ModList> coreModList() const; + std::shared_ptr<ModList> resourcePackList() const override; + std::shared_ptr<ModList> texturePackList() const override; + std::shared_ptr<WorldList> worldList() const override; + virtual QList<Mod> getJarMods() const override; + virtual void createProfile(); + + virtual QSet<QString> traits() override; + + ////// Directories and files ////// + QString jarModsDir() const; + QString resourcePacksDir() const; + QString texturePacksDir() const; + QString loaderModsDir() const; + QString coreModsDir() const; + QString libDir() const; + QString worldDir() const; + virtual QString instanceConfigFolder() const override; + + virtual std::shared_ptr<Task> createUpdateTask() override; + virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override; + virtual std::shared_ptr<Task> createJarModdingTask() override; + + virtual QString createLaunchScript(AuthSessionPtr session) override; + + virtual void cleanupAfterRun() override; + + virtual QString intendedVersionId() const override; + virtual bool setIntendedVersionId(QString version) override; + + virtual QString currentVersionId() const override; + + virtual bool shouldUpdate() const override; + virtual void setShouldUpdate(bool val) override; + + /** + * reload the profile, including version json files. + * + * throws various exceptions :3 + */ + void reloadProfile(); + + /// clears all version information in preparation for an update + void clearProfile(); + + /// get the current full version info + std::shared_ptr<MinecraftProfile> getMinecraftProfile() const; + + virtual QDir jarmodsPath() const; + virtual QDir librariesPath() const; + virtual QDir versionsPath() const; + virtual bool providesVersionFile() const; + + bool reload() override; + + virtual QStringList extraArguments() const override; + + std::shared_ptr<OneSixInstance> getSharedPtr(); + + virtual QString typeName() const override; + + bool canExport() const override + { + return true; + } + +signals: + void versionReloaded(); + +private: + QStringList processMinecraftArgs(AuthSessionPtr account); + +protected: + std::shared_ptr<MinecraftProfile> m_profile; + mutable std::shared_ptr<ModList> m_loader_mod_list; + mutable std::shared_ptr<ModList> m_core_mod_list; + mutable std::shared_ptr<ModList> m_resource_pack_list; + mutable std::shared_ptr<ModList> m_texture_pack_list; + mutable std::shared_ptr<WorldList> m_world_list; +}; + +Q_DECLARE_METATYPE(std::shared_ptr<OneSixInstance>) diff --git a/api/logic/minecraft/onesix/OneSixProfileStrategy.cpp b/api/logic/minecraft/onesix/OneSixProfileStrategy.cpp new file mode 100644 index 00000000..af42286d --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixProfileStrategy.cpp @@ -0,0 +1,418 @@ +#include "OneSixProfileStrategy.h" +#include "OneSixInstance.h" +#include "OneSixVersionFormat.h" + +#include "minecraft/VersionBuildError.h" +#include "minecraft/MinecraftVersionList.h" +#include "Env.h" +#include <FileSystem.h> + +#include <QDir> +#include <QUuid> +#include <QJsonDocument> +#include <QJsonArray> + +OneSixProfileStrategy::OneSixProfileStrategy(OneSixInstance* instance) +{ + m_instance = instance; +} + +void OneSixProfileStrategy::upgradeDeprecatedFiles() +{ + auto versionJsonPath = FS::PathCombine(m_instance->instanceRoot(), "version.json"); + auto customJsonPath = FS::PathCombine(m_instance->instanceRoot(), "custom.json"); + auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json"); + + QString sourceFile; + QString renameFile; + + // convert old crap. + if(QFile::exists(customJsonPath)) + { + sourceFile = customJsonPath; + renameFile = versionJsonPath; + } + else if(QFile::exists(versionJsonPath)) + { + sourceFile = versionJsonPath; + } + if(!sourceFile.isEmpty() && !QFile::exists(mcJson)) + { + if(!FS::ensureFilePathExists(mcJson)) + { + qWarning() << "Couldn't create patches folder for" << m_instance->name(); + return; + } + if(!renameFile.isEmpty() && QFile::exists(renameFile)) + { + if(!QFile::rename(renameFile, renameFile + ".old")) + { + qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << m_instance->name(); + return; + } + } + auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false); + ProfileUtils::removeLwjglFromPatch(file); + file->fileId = "net.minecraft"; + file->version = file->minecraftVersion; + file->name = "Minecraft"; + auto data = OneSixVersionFormat::versionFileToJson(file, false).toJson(); + QSaveFile newPatchFile(mcJson); + if(!newPatchFile.open(QIODevice::WriteOnly)) + { + newPatchFile.cancelWriting(); + qWarning() << "Couldn't open main patch for writing in" << m_instance->name(); + return; + } + newPatchFile.write(data); + if(!newPatchFile.commit()) + { + qWarning() << "Couldn't save main patch in" << m_instance->name(); + return; + } + if(!QFile::rename(sourceFile, sourceFile + ".old")) + { + qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << m_instance->name(); + return; + } + } +} + +void OneSixProfileStrategy::loadDefaultBuiltinPatches() +{ + { + auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json"); + // load up the base minecraft patch + ProfilePatchPtr minecraftPatch; + if(QFile::exists(mcJson)) + { + auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false); + if(file->version.isEmpty()) + { + file->version = m_instance->intendedVersionId(); + } + file->setVanilla(false); + file->setRevertible(true); + minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(file); + } + else + { + auto mcversion = ENV.getVersion("net.minecraft", m_instance->intendedVersionId()); + minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(mcversion); + } + if (!minecraftPatch) + { + throw VersionIncomplete("net.minecraft"); + } + minecraftPatch->setOrder(-2); + profile->appendPatch(minecraftPatch); + } + + { + auto lwjglJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "org.lwjgl.json"); + ProfilePatchPtr lwjglPatch; + if(QFile::exists(lwjglJson)) + { + auto file = ProfileUtils::parseJsonFile(QFileInfo(lwjglJson), false); + file->setVanilla(false); + file->setRevertible(true); + lwjglPatch = std::dynamic_pointer_cast<ProfilePatch>(file); + } + else + { + // NOTE: this is obviously fake, is fixed in unstable. + QResource LWJGL(":/versions/LWJGL/2.9.1.json"); + auto lwjgl = ProfileUtils::parseJsonFile(LWJGL.absoluteFilePath(), false); + lwjgl->setVanilla(true); + lwjgl->setCustomizable(true); + lwjglPatch = std::dynamic_pointer_cast<ProfilePatch>(lwjgl); + } + if (!lwjglPatch) + { + throw VersionIncomplete("org.lwjgl"); + } + lwjglPatch->setOrder(-1); + profile->appendPatch(lwjglPatch); + } +} + +void OneSixProfileStrategy::loadUserPatches() +{ + // load all patches, put into map for ordering, apply in the right order + ProfileUtils::PatchOrder userOrder; + ProfileUtils::readOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), userOrder); + QDir patches(FS::PathCombine(m_instance->instanceRoot(),"patches")); + QSet<QString> seen_extra; + + // first, load things by sort order. + for (auto id : userOrder) + { + // ignore builtins + if (id == "net.minecraft") + continue; + if (id == "org.lwjgl") + continue; + // parse the file + QString filename = patches.absoluteFilePath(id + ".json"); + QFileInfo finfo(filename); + if(!finfo.exists()) + { + qDebug() << "Patch file " << filename << " was deleted by external means..."; + continue; + } + qDebug() << "Reading" << filename << "by user order"; + VersionFilePtr file = ProfileUtils::parseJsonFile(finfo, false); + // sanity check. prevent tampering with files. + if (file->fileId != id) + { + file->addProblem(PROBLEM_WARNING, QObject::tr("load id %1 does not match internal id %2").arg(id, file->fileId)); + seen_extra.insert(file->fileId); + } + file->setRemovable(true); + file->setMovable(true); + profile->appendPatch(file); + } + // now load the rest by internal preference. + QMultiMap<int, VersionFilePtr> files; + for (auto info : patches.entryInfoList(QStringList() << "*.json", QDir::Files)) + { + // parse the file + qDebug() << "Reading" << info.fileName(); + auto file = ProfileUtils::parseJsonFile(info, true); + // ignore builtins + if (file->fileId == "net.minecraft") + continue; + if (file->fileId == "org.lwjgl") + continue; + // do not load versions with broken IDs twice + if(seen_extra.contains(file->fileId)) + continue; + // do not load what we already loaded in the first pass + if (userOrder.contains(file->fileId)) + continue; + file->setRemovable(true); + file->setMovable(true); + files.insert(file->order, file); + } + QSet<int> seen; + for (auto order : files.keys()) + { + if(seen.contains(order)) + continue; + seen.insert(order); + const auto &values = files.values(order); + if(values.size() == 1) + { + profile->appendPatch(values[0]); + continue; + } + for(auto &file: values) + { + QStringList list; + for(auto &file2: values) + { + if(file != file2) + list.append(file2->name); + } + file->addProblem(PROBLEM_WARNING, QObject::tr("%1 has the same order as the following components:\n%2").arg(file->name, list.join(", "))); + profile->appendPatch(file); + } + } +} + + +void OneSixProfileStrategy::load() +{ + profile->clearPatches(); + + upgradeDeprecatedFiles(); + loadDefaultBuiltinPatches(); + loadUserPatches(); +} + +bool OneSixProfileStrategy::saveOrder(ProfileUtils::PatchOrder order) +{ + return ProfileUtils::writeOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), order); +} + +bool OneSixProfileStrategy::resetOrder() +{ + return QDir(m_instance->instanceRoot()).remove("order.json"); +} + +bool OneSixProfileStrategy::removePatch(ProfilePatchPtr patch) +{ + bool ok = true; + // first, remove the patch file. this ensures it's not used anymore + auto fileName = patch->getFilename(); + if(fileName.size()) + { + QFile patchFile(fileName); + if(patchFile.exists() && !patchFile.remove()) + { + qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString(); + return false; + } + } + + + auto preRemoveJarMod = [&](JarmodPtr jarMod) -> bool + { + QString fullpath = FS::PathCombine(m_instance->jarModsDir(), jarMod->name); + QFileInfo finfo (fullpath); + if(finfo.exists()) + { + QFile jarModFile(fullpath); + if(!jarModFile.remove()) + { + qCritical() << "File" << fullpath << "could not be removed because:" << jarModFile.errorString(); + return false; + } + return true; + } + return true; + }; + + for(auto &jarmod: patch->getJarMods()) + { + ok &= preRemoveJarMod(jarmod); + } + return ok; +} + +bool OneSixProfileStrategy::customizePatch(ProfilePatchPtr patch) +{ + if(patch->isCustom()) + { + return false; + } + + auto filename = FS::PathCombine(m_instance->instanceRoot(), "patches" , patch->getID() + ".json"); + if(!FS::ensureFilePathExists(filename)) + { + return false; + } + try + { + QSaveFile jsonFile(filename); + if(!jsonFile.open(QIODevice::WriteOnly)) + { + return false; + } + auto vfile = patch->getVersionFile(); + if(!vfile) + { + return false; + } + auto document = OneSixVersionFormat::versionFileToJson(vfile, true); + jsonFile.write(document.toJson()); + if(!jsonFile.commit()) + { + return false; + } + load(); + } + catch (VersionIncomplete &error) + { + qDebug() << "Version was incomplete:" << error.cause(); + } + catch (Exception &error) + { + qWarning() << "Version could not be loaded:" << error.cause(); + } + return true; +} + +bool OneSixProfileStrategy::revertPatch(ProfilePatchPtr patch) +{ + if(!patch->isCustom()) + { + // already not custom + return true; + } + auto filename = patch->getFilename(); + if(!QFile::exists(filename)) + { + // already gone / not custom + return true; + } + // just kill the file and reload + bool result = QFile::remove(filename); + try + { + load(); + } + catch (VersionIncomplete &error) + { + qDebug() << "Version was incomplete:" << error.cause(); + } + catch (Exception &error) + { + qWarning() << "Version could not be loaded:" << error.cause(); + } + return result; +} + +bool OneSixProfileStrategy::installJarMods(QStringList filepaths) +{ + QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + if (!FS::ensureFolderPathExists(m_instance->jarModsDir())) + { + return false; + } + + for(auto filepath:filepaths) + { + QFileInfo sourceInfo(filepath); + auto uuid = QUuid::createUuid(); + QString id = uuid.toString().remove('{').remove('}'); + QString target_filename = id + ".jar"; + QString target_id = "org.multimc.jarmod." + id; + QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; + QString finalPath = FS::PathCombine(m_instance->jarModsDir(), target_filename); + + QFileInfo targetInfo(finalPath); + if(targetInfo.exists()) + { + return false; + } + + if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath())) + { + return false; + } + + auto f = std::make_shared<VersionFile>(); + auto jarMod = std::make_shared<Jarmod>(); + jarMod->name = target_filename; + jarMod->originalName = sourceInfo.completeBaseName(); + f->jarMods.append(jarMod); + f->name = target_name; + f->fileId = target_id; + f->order = profile->getFreeOrderNumber(); + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + f->filename = patchFileName; + f->setMovable(true); + f->setRemovable(true); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f, true).toJson()); + file.close(); + profile->appendPatch(f); + } + profile->saveCurrentOrder(); + profile->reapplyPatches(); + return true; +} + diff --git a/api/logic/minecraft/onesix/OneSixProfileStrategy.h b/api/logic/minecraft/onesix/OneSixProfileStrategy.h new file mode 100644 index 00000000..96c1ba7b --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixProfileStrategy.h @@ -0,0 +1,26 @@ +#pragma once +#include "minecraft/ProfileStrategy.h" + +class OneSixInstance; + +class OneSixProfileStrategy : public ProfileStrategy +{ +public: + OneSixProfileStrategy(OneSixInstance * instance); + virtual ~OneSixProfileStrategy() {}; + virtual void load() override; + virtual bool resetOrder() override; + virtual bool saveOrder(ProfileUtils::PatchOrder order) override; + virtual bool installJarMods(QStringList filepaths) override; + virtual bool removePatch(ProfilePatchPtr patch) override; + virtual bool customizePatch(ProfilePatchPtr patch) override; + virtual bool revertPatch(ProfilePatchPtr patch) override; + +protected: + virtual void loadDefaultBuiltinPatches(); + virtual void loadUserPatches(); + void upgradeDeprecatedFiles(); + +protected: + OneSixInstance *m_instance; +}; diff --git a/api/logic/minecraft/onesix/OneSixUpdate.cpp b/api/logic/minecraft/onesix/OneSixUpdate.cpp new file mode 100644 index 00000000..1c2cd196 --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixUpdate.cpp @@ -0,0 +1,342 @@ +/* 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 <minecraft/forge/ForgeXzDownload.h> +#include "OneSixUpdate.h" +#include "OneSixInstance.h" + +#include <QtNetwork> + +#include <QFile> +#include <QFileInfo> +#include <QTextStream> +#include <QDataStream> +#include <JlCompress.h> + +#include "BaseInstance.h" +#include "minecraft/MinecraftVersionList.h" +#include "minecraft/MinecraftProfile.h" +#include "minecraft/Library.h" +#include "net/URLConstants.h" +#include "minecraft/AssetsUtils.h" +#include "Exception.h" +#include "MMCZip.h" +#include <FileSystem.h> + +OneSixUpdate::OneSixUpdate(OneSixInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void OneSixUpdate::executeTask() +{ + // Make directories + QDir mcDir(m_inst->minecraftRoot()); + if (!mcDir.exists() && !mcDir.mkpath(".")) + { + emitFailed(tr("Failed to create folder for minecraft binaries.")); + return; + } + + // Get a pointer to the version object that corresponds to the instance's version. + targetVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", m_inst->intendedVersionId())); + if (targetVersion == nullptr) + { + // don't do anything if it was invalid + emitFailed(tr("The specified Minecraft version is invalid. Choose a different one.")); + return; + } + if (m_inst->providesVersionFile() || !targetVersion->needsUpdate()) + { + qDebug() << "Instance either provides a version file or doesn't need an update."; + jarlibStart(); + return; + } + versionUpdateTask = std::dynamic_pointer_cast<MinecraftVersionList>(ENV.getVersionList("net.minecraft"))->createUpdateTask(m_inst->intendedVersionId()); + if (!versionUpdateTask) + { + qDebug() << "Didn't spawn an update task."; + jarlibStart(); + return; + } + connect(versionUpdateTask.get(), SIGNAL(succeeded()), SLOT(jarlibStart())); + connect(versionUpdateTask.get(), &NetJob::failed, this, &OneSixUpdate::versionUpdateFailed); + connect(versionUpdateTask.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + setStatus(tr("Getting the version files from Mojang...")); + versionUpdateTask->start(); +} + +void OneSixUpdate::versionUpdateFailed(QString reason) +{ + emitFailed(reason); +} + +void OneSixUpdate::assetIndexStart() +{ + setStatus(tr("Updating assets index...")); + OneSixInstance *inst = (OneSixInstance *)m_inst; + auto profile = inst->getMinecraftProfile(); + auto assets = profile->getMinecraftAssets(); + QUrl indexUrl = assets->url; + QString localPath = assets->id + ".json"; + auto job = new NetJob(tr("Asset index for %1").arg(inst->name())); + + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("asset_indexes", localPath); + entry->setStale(true); + job->addNetAction(CacheDownload::make(indexUrl, entry)); + jarlibDownloadJob.reset(job); + + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetIndexFinished())); + connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::assetIndexFailed); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + + qDebug() << m_inst->name() << ": Starting asset index download"; + jarlibDownloadJob->start(); +} + +void OneSixUpdate::assetIndexFinished() +{ + AssetsIndex index; + qDebug() << m_inst->name() << ": Finished asset index download"; + + OneSixInstance *inst = (OneSixInstance *)m_inst; + auto profile = inst->getMinecraftProfile(); + auto assets = profile->getMinecraftAssets(); + + QString asset_fname = "assets/indexes/" + assets->id + ".json"; + // FIXME: this looks like a job for a generic validator based on json schema? + if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, &index)) + { + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); + metacache->evictEntry(entry); + emitFailed(tr("Failed to read the assets index!")); + } + + auto job = index.getDownloadJob(); + if(job) + { + setStatus(tr("Getting the assets files from Mojang...")); + jarlibDownloadJob = job; + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetsFinished())); + connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::assetsFailed); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + jarlibDownloadJob->start(); + return; + } + assetsFinished(); +} + +void OneSixUpdate::assetIndexFailed(QString reason) +{ + qDebug() << m_inst->name() << ": Failed asset index download"; + emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); +} + +void OneSixUpdate::assetsFinished() +{ + emitSucceeded(); +} + +void OneSixUpdate::assetsFailed(QString reason) +{ + emitFailed(tr("Failed to download assets:\n%1").arg(reason)); +} + +void OneSixUpdate::jarlibStart() +{ + setStatus(tr("Getting the library files from Mojang...")); + qDebug() << m_inst->name() << ": downloading libraries"; + OneSixInstance *inst = (OneSixInstance *)m_inst; + inst->reloadProfile(); + if(inst->flags() & BaseInstance::VersionBrokenFlag) + { + emitFailed(tr("Failed to load the version description files - check the instance for errors.")); + return; + } + + // Build a list of URLs that will need to be downloaded. + std::shared_ptr<MinecraftProfile> profile = inst->getMinecraftProfile(); + // minecraft.jar for this version + { + QString version_id = profile->getMinecraftVersion(); + QString localPath = version_id + "/" + version_id + ".jar"; + QString urlstr = profile->getMainJarUrl(); + + auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); + + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + job->addNetAction(CacheDownload::make(QUrl(urlstr), entry)); + jarlibDownloadJob.reset(job); + } + + auto libs = profile->getLibraries(); + + auto metacache = ENV.metacache(); + QList<LibraryPtr> brokenLocalLibs; + + QStringList failedFiles; + for (auto lib : libs) + { + auto dls = lib->getDownloads(currentSystem, metacache.get(), failedFiles); + for(auto dl : dls) + { + jarlibDownloadJob->addNetAction(dl); + } + } + if (!brokenLocalLibs.empty()) + { + jarlibDownloadJob.reset(); + + QString failed_all = failedFiles.join("\n"); + emitFailed(tr("Some libraries marked as 'local' are missing their jar " + "files:\n%1\n\nYou'll have to correct this problem manually. If this is " + "an externally tracked instance, make sure to run it at least once " + "outside of MultiMC.").arg(failed_all)); + return; + } + + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(jarlibFinished())); + connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::jarlibFailed); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + + jarlibDownloadJob->start(); +} + +void OneSixUpdate::jarlibFinished() +{ + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<MinecraftProfile> profile = inst->getMinecraftProfile(); + + if (profile->hasTrait("legacyFML")) + { + fmllibsStart(); + } + else + { + assetIndexStart(); + } +} + +void OneSixUpdate::jarlibFailed(QString reason) +{ + QStringList failed = jarlibDownloadJob->getFailedFiles(); + QString failed_all = failed.join("\n"); + emitFailed( + tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); +} + +void OneSixUpdate::fmllibsStart() +{ + // Get the mod list + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<MinecraftProfile> profile = inst->getMinecraftProfile(); + bool forge_present = false; + + QString version = inst->intendedVersionId(); + auto &fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + assetIndexStart(); + return; + } + + auto &libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + forge_present = (profile->versionPatch("net.minecraftforge") != nullptr); + // we don't... + if (!forge_present) + { + assetIndexStart(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto &lib : libList) + { + QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + assetIndexStart(); + return; + } + + // download missing libs to our place + setStatus(tr("Dowloading FML libraries...")); + auto dljob = new NetJob("FML libraries"); + auto metacache = ENV.metacache(); + for (auto &lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename + : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; + dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry)); + } + + connect(dljob, SIGNAL(succeeded()), SLOT(fmllibsFinished())); + connect(dljob, &NetJob::failed, this, &OneSixUpdate::fmllibsFailed); + connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + legacyDownloadJob.reset(dljob); + legacyDownloadJob->start(); +} + +void OneSixUpdate::fmllibsFinished() +{ + legacyDownloadJob.reset(); + if (!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + OneSixInstance *inst = (OneSixInstance *)m_inst; + auto metacache = ENV.metacache(); + int index = 0; + for (auto &lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = FS::PathCombine(inst->libDir(), lib.filename); + if (!FS::ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + assetIndexStart(); +} + +void OneSixUpdate::fmllibsFailed(QString reason) +{ + emitFailed(tr("Game update failed: it was impossible to fetch the required FML libraries.\nReason:\n%1").arg(reason)); + return; +} + diff --git a/api/logic/minecraft/onesix/OneSixUpdate.h b/api/logic/minecraft/onesix/OneSixUpdate.h new file mode 100644 index 00000000..b5195364 --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixUpdate.h @@ -0,0 +1,67 @@ +/* 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 <QObject> +#include <QList> +#include <QUrl> + +#include "net/NetJob.h" +#include "tasks/Task.h" +#include "minecraft/VersionFilterData.h" +#include <quazip.h> + +class MinecraftVersion; +class OneSixInstance; + +class OneSixUpdate : public Task +{ + Q_OBJECT +public: + explicit OneSixUpdate(OneSixInstance *inst, QObject *parent = 0); + virtual void executeTask(); + +private +slots: + void versionUpdateFailed(QString reason); + + void jarlibStart(); + void jarlibFinished(); + void jarlibFailed(QString reason); + + void fmllibsStart(); + void fmllibsFinished(); + void fmllibsFailed(QString reason); + + void assetIndexStart(); + void assetIndexFinished(); + void assetIndexFailed(QString reason); + + void assetsFinished(); + void assetsFailed(QString reason); + +private: + NetJobPtr jarlibDownloadJob; + NetJobPtr legacyDownloadJob; + + /// target version, determined during this task + std::shared_ptr<MinecraftVersion> targetVersion; + /// the task that is spawned for version updates + std::shared_ptr<Task> versionUpdateTask; + + OneSixInstance *m_inst = nullptr; + QList<FMLlib> fmlLibsToProcess; +}; diff --git a/api/logic/minecraft/onesix/OneSixVersionFormat.cpp b/api/logic/minecraft/onesix/OneSixVersionFormat.cpp new file mode 100644 index 00000000..541fb109 --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixVersionFormat.cpp @@ -0,0 +1,225 @@ +#include "OneSixVersionFormat.h" +#include <Json.h> +#include "minecraft/ParseUtils.h" +#include <minecraft/MinecraftVersion.h> +#include <minecraft/VersionBuildError.h> +#include <minecraft/MojangVersionFormat.h> + +using namespace Json; + +static void readString(const QJsonObject &root, const QString &key, QString &variable) +{ + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } +} + +LibraryPtr OneSixVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +{ + LibraryPtr out = MojangVersionFormat::libraryFromJson(libObj, filename); + readString(libObj, "MMC-hint", out->m_hint); + readString(libObj, "MMC-absulute_url", out->m_absoluteURL); + readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); + return out; +} + +QJsonObject OneSixVersionFormat::libraryToJson(Library *library) +{ + QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); + if (library->m_absoluteURL.size()) + libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); + if (library->m_hint.size()) + libRoot.insert("MMC-hint", library->m_hint); + return libRoot; +} + +VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + if (requireOrder) + { + if (root.contains("order")) + { + out->order = requireInteger(root.value("order")); + } + else + { + // FIXME: evaluate if we don't want to throw exceptions here instead + qCritical() << filename << "doesn't contain an order field"; + } + } + + out->name = root.value("name").toString(); + out->fileId = root.value("fileId").toString(); + out->version = root.value("version").toString(); + out->dependsOnMinecraftVersion = root.value("mcVersion").toString(); + out->filename = filename; + + MojangVersionFormat::readVersionProperties(root, out.get()); + + // added for legacy Minecraft window embedding, TODO: remove + readString(root, "appletClass", out->appletClass); + + if (root.contains("+tweakers")) + { + for (auto tweakerVal : requireArray(root.value("+tweakers"))) + { + out->addTweakers.append(requireString(tweakerVal)); + } + } + + if (root.contains("+traits")) + { + for (auto tweakerVal : requireArray(root.value("+traits"))) + { + out->traits.insert(requireString(tweakerVal)); + } + } + + if (root.contains("+jarMods")) + { + for (auto libVal : requireArray(root.value("+jarMods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::jarModFromJson(libObj, filename, out->name); + if(lib->originalName.isEmpty()) + { + auto fixed = out->name; + fixed.remove(" (jar mod)"); + lib->originalName = out->name; + } + // and add to jar mods + out->jarMods.append(lib); + } + } + + auto readLibs = [&](const char * which) + { + for (auto libVal : requireArray(root.value(which))) + { + QJsonObject libObj = requireObject(libVal); + // parse the library + auto lib = libraryFromJson(libObj, filename); + out->libraries.append(lib); + } + }; + bool hasPlusLibs = root.contains("+libraries"); + bool hasLibs = root.contains("libraries"); + if (hasPlusLibs && hasLibs) + { + out->addProblem(PROBLEM_WARNING, QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); + readLibs("libraries"); + readLibs("+libraries"); + } + else if (hasLibs) + { + readLibs("libraries"); + } + else if(hasPlusLibs) + { + readLibs("+libraries"); + } + + /* removed features that shouldn't be used */ + if (root.contains("tweakers")) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element 'tweakers'")); + } + if (root.contains("-libraries")) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '-libraries'")); + } + if (root.contains("-tweakers")) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '-tweakers'")); + } + if (root.contains("-minecraftArguments")) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '-minecraftArguments'")); + } + if (root.contains("+minecraftArguments")) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '+minecraftArguments'")); + } + return out; +} + +QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch, bool saveOrder) +{ + QJsonObject root; + if (saveOrder) + { + root.insert("order", patch->order); + } + writeString(root, "name", patch->name); + writeString(root, "fileId", patch->fileId); + writeString(root, "version", patch->version); + writeString(root, "mcVersion", patch->dependsOnMinecraftVersion); + + MojangVersionFormat::writeVersionProperties(patch.get(), root); + + writeString(root, "appletClass", patch->appletClass); + writeStringList(root, "+tweakers", patch->addTweakers); + writeStringList(root, "+traits", patch->traits.toList()); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value: patch->libraries) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("+libraries", array); + } + if (!patch->jarMods.isEmpty()) + { + QJsonArray array; + for (auto value: patch->jarMods) + { + array.append(OneSixVersionFormat::jarModtoJson(value.get())); + } + root.insert("+jarMods", array); + } + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +JarmodPtr OneSixVersionFormat::jarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName) +{ + JarmodPtr out(new Jarmod()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + + "contains a jarmod that doesn't have a 'name' field"); + } + out->name = libObj.value("name").toString(); + out->originalName = libObj.value("originalName").toString(); + return out; +} + +QJsonObject OneSixVersionFormat::jarModtoJson(Jarmod *jarmod) +{ + QJsonObject out; + writeString(out, "name", jarmod->name); + if(!jarmod->originalName.isEmpty()) + { + writeString(out, "originalName", jarmod->originalName); + } + return out; +} diff --git a/api/logic/minecraft/onesix/OneSixVersionFormat.h b/api/logic/minecraft/onesix/OneSixVersionFormat.h new file mode 100644 index 00000000..5696e79e --- /dev/null +++ b/api/logic/minecraft/onesix/OneSixVersionFormat.h @@ -0,0 +1,22 @@ +#pragma once + +#include <minecraft/VersionFile.h> +#include <minecraft/MinecraftProfile.h> +#include <minecraft/Library.h> +#include <QJsonDocument> + +class OneSixVersionFormat +{ +public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder); + static QJsonDocument versionFileToJson(const VersionFilePtr &patch, bool saveOrder); + + // libraries + static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static QJsonObject libraryToJson(Library *library); + + // jar mods + static JarmodPtr jarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName); + static QJsonObject jarModtoJson(Jarmod * jarmod); +}; |