diff options
Diffstat (limited to 'logic/minecraft')
25 files changed, 3833 insertions, 0 deletions
diff --git a/logic/minecraft/InstanceVersion.cpp b/logic/minecraft/InstanceVersion.cpp new file mode 100644 index 00000000..ca0e3796 --- /dev/null +++ b/logic/minecraft/InstanceVersion.cpp @@ -0,0 +1,537 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QDebug> +#include <QFile> +#include <QDir> +#include <QUuid> +#include <QJsonDocument> +#include <QJsonArray> +#include <pathutils.h> + +#include "logic/minecraft/InstanceVersion.h" +#include "logic/minecraft/VersionBuilder.h" +#include "logic/OneSixInstance.h" + +InstanceVersion::InstanceVersion(OneSixInstance *instance, QObject *parent) + : QAbstractListModel(parent), m_instance(instance) +{ + clear(); +} + +void InstanceVersion::reload(const QStringList &external) +{ + m_externalPatches = external; + beginResetModel(); + VersionBuilder::build(this, m_instance, m_externalPatches); + reapply(true); + endResetModel(); +} + +void InstanceVersion::clear() +{ + id.clear(); + m_updateTimeString.clear(); + m_updateTime = QDateTime(); + m_releaseTimeString.clear(); + m_releaseTime = QDateTime(); + type.clear(); + assets.clear(); + processArguments.clear(); + minecraftArguments.clear(); + minimumLauncherVersion = 0xDEADBEAF; + mainClass.clear(); + appletClass.clear(); + libraries.clear(); + tweakers.clear(); + jarMods.clear(); + traits.clear(); +} + +bool InstanceVersion::canRemove(const int index) const +{ + return VersionPatches.at(index)->isMoveable(); +} + +bool InstanceVersion::preremove(VersionPatchPtr patch) +{ + bool ok = true; + for(auto & jarmod: patch->getJarMods()) + { + QString fullpath =PathCombine(m_instance->jarModsDir(), jarmod->name); + QFileInfo finfo (fullpath); + if(finfo.exists()) + ok &= QFile::remove(fullpath); + } + return ok; +} + +bool InstanceVersion::remove(const int index) +{ + if (!canRemove(index)) + return false; + if(!preremove(VersionPatches[index])) + { + return false; + } + if(!QFile::remove(VersionPatches.at(index)->getPatchFilename())) + return false; + beginRemoveRows(QModelIndex(), index, index); + VersionPatches.removeAt(index); + endRemoveRows(); + reapply(true); + saveCurrentOrder(); + return true; +} + +bool InstanceVersion::remove(const QString id) +{ + int i = 0; + for (auto patch : VersionPatches) + { + if (patch->getPatchID() == id) + { + return remove(i); + } + i++; + } + return false; +} + +QString InstanceVersion::versionFileId(const int index) const +{ + if (index < 0 || index >= VersionPatches.size()) + { + return QString(); + } + return VersionPatches.at(index)->getPatchID(); +} + +VersionPatchPtr InstanceVersion::versionPatch(const QString &id) +{ + for (auto file : VersionPatches) + { + if (file->getPatchID() == id) + { + return file; + } + } + return 0; +} + +VersionPatchPtr InstanceVersion::versionPatch(int index) +{ + if(index < 0 || index >= VersionPatches.size()) + return 0; + return VersionPatches[index]; +} + + +bool InstanceVersion::hasJarMods() +{ + return !jarMods.isEmpty(); +} + +bool InstanceVersion::hasFtbPack() +{ + return versionPatch("org.multimc.ftb.pack.json") != nullptr; +} + +bool InstanceVersion::removeFtbPack() +{ + return remove("org.multimc.ftb.pack.json"); +} + +bool InstanceVersion::isVanilla() +{ + QDir patches(PathCombine(m_instance->instanceRoot(), "patches/")); + if(VersionPatches.size() > 1) + return false; + if(QFile::exists(PathCombine(m_instance->instanceRoot(), "custom.json"))) + return false; + if(QFile::exists(PathCombine(m_instance->instanceRoot(), "version.json"))) + return false; + return true; +} + +bool InstanceVersion::revertToVanilla() +{ + beginResetModel(); + // remove custom.json, if present + QString customPath = PathCombine(m_instance->instanceRoot(), "custom.json"); + if(QFile::exists(customPath)) + { + if(!QFile::remove(customPath)) + { + endResetModel(); + return false; + } + } + // remove version.json, if present + QString versionPath = PathCombine(m_instance->instanceRoot(), "version.json"); + if(QFile::exists(versionPath)) + { + if(!QFile::remove(versionPath)) + { + endResetModel(); + return false; + } + } + // remove patches, if present + auto it = VersionPatches.begin(); + while (it != VersionPatches.end()) + { + if ((*it)->isMoveable()) + { + if(!preremove(*it)) + { + endResetModel(); + saveCurrentOrder(); + return false; + } + if(!QFile::remove((*it)->getPatchFilename())) + { + endResetModel(); + saveCurrentOrder(); + return false; + } + it = VersionPatches.erase(it); + } + else + it++; + } + reapply(true); + endResetModel(); + saveCurrentOrder(); + return true; +} + +bool InstanceVersion::hasDeprecatedVersionFiles() +{ + if(QFile::exists(PathCombine(m_instance->instanceRoot(), "custom.json"))) + return true; + if(QFile::exists(PathCombine(m_instance->instanceRoot(), "version.json"))) + return true; + return false; +} + +bool InstanceVersion::removeDeprecatedVersionFiles() +{ + beginResetModel(); + // remove custom.json, if present + QString customPath = PathCombine(m_instance->instanceRoot(), "custom.json"); + if(QFile::exists(customPath)) + { + if(!QFile::remove(customPath)) + { + endResetModel(); + return false; + } + } + // remove version.json, if present + QString versionPath = PathCombine(m_instance->instanceRoot(), "version.json"); + if(QFile::exists(versionPath)) + { + if(!QFile::remove(versionPath)) + { + endResetModel(); + return false; + } + } + endResetModel(); + return true; +} + +QList<std::shared_ptr<OneSixLibrary> > InstanceVersion::getActiveNormalLibs() +{ + QList<std::shared_ptr<OneSixLibrary> > output; + for (auto lib : libraries) + { + if (lib->isActive() && !lib->isNative()) + { + output.append(lib); + } + } + return output; +} +QList<std::shared_ptr<OneSixLibrary> > InstanceVersion::getActiveNativeLibs() +{ + QList<std::shared_ptr<OneSixLibrary> > output; + for (auto lib : libraries) + { + if (lib->isActive() && lib->isNative()) + { + output.append(lib); + } + } + return output; +} + +std::shared_ptr<InstanceVersion> InstanceVersion::fromJson(const QJsonObject &obj) +{ + std::shared_ptr<InstanceVersion> version(new InstanceVersion(0)); + try + { + VersionBuilder::readJsonAndApplyToVersion(version.get(), obj); + } + catch(MMCError & err) + { + return 0; + } + return version; +} + +QVariant InstanceVersion::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= VersionPatches.size()) + return QVariant(); + + if (role == Qt::DisplayRole) + { + switch (column) + { + case 0: + return VersionPatches.at(row)->getPatchName(); + case 1: + return VersionPatches.at(row)->getPatchVersion(); + default: + return QVariant(); + } + } + return QVariant(); +} +QVariant InstanceVersion::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 InstanceVersion::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +int InstanceVersion::rowCount(const QModelIndex &parent) const +{ + return VersionPatches.size(); +} + +int InstanceVersion::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +void InstanceVersion::saveCurrentOrder() const +{ + PatchOrder order; + for(auto item: VersionPatches) + { + if(!item->isMoveable()) + continue; + order.append(item->getPatchID()); + } + VersionBuilder::writeOverrideOrders(m_instance, order); +} + +void InstanceVersion::move(const int index, const MoveDirection direction) +{ + int theirIndex; + if (direction == MoveUp) + { + theirIndex = index - 1; + } + else + { + theirIndex = index + 1; + } + + if (index < 0 || index >= VersionPatches.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); + VersionPatches.swap(index, theirIndex); + endMoveRows(); + saveCurrentOrder(); + reapply(); +} +void InstanceVersion::resetOrder() +{ + QDir(m_instance->instanceRoot()).remove("order.json"); + reload(m_externalPatches); +} + +void InstanceVersion::reapply(const bool alreadyReseting) +{ + clear(); + for(auto file: VersionPatches) + { + file->applyTo(this); + } + finalize(); +} + +void InstanceVersion::finalize() +{ + // HACK: deny april fools. my head hurts enough already. + QDate now = QDate::currentDate(); + bool isAprilFools = now.month() == 4 && now.day() == 1; + if (assets.endsWith("_af") && !isAprilFools) + { + assets = assets.left(assets.length() - 3); + } + if (assets.isEmpty()) + { + assets = "legacy"; + } + auto finalizeArguments = [&]( QString & minecraftArguments, const QString & processArguments ) -> void + { + if (!minecraftArguments.isEmpty()) + return; + QString toCompare = processArguments.toLower(); + if (toCompare == "legacy") + { + minecraftArguments = " ${auth_player_name} ${auth_session}"; + } + else if (toCompare == "username_session") + { + minecraftArguments = "--username ${auth_player_name} --session ${auth_session}"; + } + else if (toCompare == "username_session_version") + { + minecraftArguments = "--username ${auth_player_name} " + "--session ${auth_session} " + "--version ${profile_name}"; + } + }; + finalizeArguments(vanillaMinecraftArguments, vanillaProcessArguments); + finalizeArguments(minecraftArguments, processArguments); +} + +void InstanceVersion::installJarMods(QStringList selectedFiles) +{ + for(auto filename: selectedFiles) + { + installJarModByFilename(filename); + } +} + +void InstanceVersion::installJarModByFilename(QString filepath) +{ + QString patchDir = PathCombine(m_instance->instanceRoot(), "patches"); + if(!ensureFolderPathExists(patchDir)) + { + // THROW... + return; + } + + if (!ensureFolderPathExists(m_instance->jarModsDir())) + { + // THROW... + return; + } + + 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 = PathCombine(m_instance->jarModsDir(), target_filename); + + QFileInfo targetInfo(finalPath); + if(targetInfo.exists()) + { + // THROW + return; + } + + if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath())) + { + // THROW + return; + } + + auto f = std::make_shared<VersionFile>(); + auto jarMod = std::make_shared<Jarmod>(); + jarMod->name = target_filename; + f->jarMods.append(jarMod); + f->name = target_name; + f->fileId = target_id; + f->order = getFreeOrderNumber(); + + QFile file(PathCombine(patchDir, target_id + ".json")); + if (!file.open(QFile::WriteOnly)) + { + QLOG_ERROR() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return; + // THROW + } + file.write(f->toJson(true).toJson()); + file.close(); + int index = VersionPatches.size(); + beginInsertRows(QModelIndex(), index, index); + VersionPatches.append(f); + endInsertRows(); + saveCurrentOrder(); +} + +int InstanceVersion::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: VersionPatches) + { + int order = thing->getOrder(); + if(order > largest) + largest = order; + } + return largest + 1; +} diff --git a/logic/minecraft/InstanceVersion.h b/logic/minecraft/InstanceVersion.h new file mode 100644 index 00000000..6b69ab47 --- /dev/null +++ b/logic/minecraft/InstanceVersion.h @@ -0,0 +1,184 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QAbstractListModel> + +#include <QString> +#include <QList> +#include <memory> + +#include "OneSixLibrary.h" +#include "VersionFile.h" +#include "JarMod.h" + +class OneSixInstance; + +class InstanceVersion : public QAbstractListModel +{ + Q_OBJECT +public: + explicit InstanceVersion(OneSixInstance *instance, QObject *parent = 0); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + virtual int columnCount(const QModelIndex &parent) const; + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + + void reload(const QStringList &external = QStringList()); + void clear(); + + bool canRemove(const int index) const; + + QString versionFileId(const int index) const; + + // is this version unmodded vanilla minecraft? + bool isVanilla(); + // remove any customizations on top of vanilla + bool revertToVanilla(); + + // does this version consist of obsolete files? + bool hasDeprecatedVersionFiles(); + // remove obsolete files + bool removeDeprecatedVersionFiles(); + + // does this version have an FTB pack patch file? + bool hasFtbPack(); + // remove FTB pack + bool removeFtbPack(); + + // does this version have any jar mods? + bool hasJarMods(); + void installJarMods(QStringList selectedFiles); + void installJarModByFilename(QString filepath); + + enum MoveDirection { MoveUp, MoveDown }; + void move(const int index, const MoveDirection direction); + void resetOrder(); + + // clears and reapplies all version files + void reapply(const bool alreadyReseting = false); + void finalize(); + +public +slots: + bool remove(const int index); + bool remove(const QString id); + +public: + QList<std::shared_ptr<OneSixLibrary>> getActiveNormalLibs(); + QList<std::shared_ptr<OneSixLibrary>> getActiveNativeLibs(); + + static std::shared_ptr<InstanceVersion> fromJson(const QJsonObject &obj); + +private: + bool preremove(VersionPatchPtr patch); + + // data members +public: + /// the ID - determines which jar to use! ACTUALLY IMPORTANT! + QString id; + + /// the time this version was actually released by Mojang, as string and as QDateTime + QString m_releaseTimeString; + QDateTime m_releaseTime; + + /// the time this version was last updated by Mojang, as string and as QDateTime + QString m_updateTimeString; + QDateTime m_updateTime; + + /// Release type - "release" or "snapshot" + QString type; + /// Assets type - "legacy" or a version ID + QString assets; + /** + * DEPRECATED: Old versions of the new vanilla launcher used this + * ex: "username_session_version" + */ + QString processArguments; + /// Same as above, but only for vanilla + QString vanillaProcessArguments; + /** + * 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 minecraftArguments; + /// Same as above, but only for vanilla + QString vanillaMinecraftArguments; + /** + * the minimum launcher version required by this version ... current is 4 (at point of + * writing) + */ + int minimumLauncherVersion = 0xDEADBEEF; + /** + * A list of all tweaker classes + */ + QStringList tweakers; + /** + * The main class to load first + */ + QString mainClass; + /** + * The applet class, for some very old minecraft releases + */ + QString appletClass; + + /// the list of libs - both active and inactive, native and java + QList<std::shared_ptr<OneSixLibrary>> libraries; + + /// same, but only vanilla. + QList<std::shared_ptr<OneSixLibrary>> vanillaLibraries; + + /// traits, collected from all the version files (version files can only add) + QSet<QString> traits; + + /// A list of jar mods. version files can add those. + QList<JarmodPtr> jarMods; + + /* + 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; + + QList<VersionPatchPtr> VersionPatches; + VersionPatchPtr versionPatch(const QString &id); + VersionPatchPtr versionPatch(int index); + +private: + QStringList m_externalPatches; + OneSixInstance *m_instance; + void saveCurrentOrder() const; + int getFreeOrderNumber(); +}; diff --git a/logic/minecraft/JarMod.cpp b/logic/minecraft/JarMod.cpp new file mode 100644 index 00000000..18a9411c --- /dev/null +++ b/logic/minecraft/JarMod.cpp @@ -0,0 +1,56 @@ +#include "JarMod.h" +#include "logic/MMCJson.h" +using namespace MMCJson; + +JarmodPtr Jarmod::fromJson(const QJsonObject &libObj, const QString &filename) +{ + 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(); + + auto readString = [libObj, filename](const QString & key, QString & variable) + { + if (libObj.contains(key)) + { + QJsonValue val = libObj.value(key); + if (!val.isString()) + { + QLOG_WARN() << key << "is not a string in" << filename << "(skipping)"; + } + else + { + variable = val.toString(); + } + } + }; + + readString("url", out->baseurl); + readString("MMC-hint", out->hint); + readString("MMC-absoluteUrl", out->absoluteUrl); + if(!out->baseurl.isEmpty() && out->absoluteUrl.isEmpty()) + { + out->absoluteUrl = out->baseurl + out->name; + } + return out; +} + +QJsonObject Jarmod::toJson() +{ + QJsonObject out; + writeString(out, "name", name); + writeString(out, "url", baseurl); + writeString(out, "MMC-absoluteUrl", absoluteUrl); + writeString(out, "MMC-hint", hint); + return out; +} + +QString Jarmod::url() +{ + if(!absoluteUrl.isEmpty()) + return absoluteUrl; + else return baseurl + name; +} diff --git a/logic/minecraft/JarMod.h b/logic/minecraft/JarMod.h new file mode 100644 index 00000000..c438dbcd --- /dev/null +++ b/logic/minecraft/JarMod.h @@ -0,0 +1,18 @@ +#pragma once +#include <QString> +#include <QJsonObject> +#include <memory> +class Jarmod; +typedef std::shared_ptr<Jarmod> JarmodPtr; +class Jarmod +{ +public: /* methods */ + static JarmodPtr fromJson(const QJsonObject &libObj, const QString &filename); + QJsonObject toJson(); + QString url(); +public: /* data */ + QString name; + QString baseurl; + QString hint; + QString absoluteUrl; +}; diff --git a/logic/minecraft/MinecraftVersion.cpp b/logic/minecraft/MinecraftVersion.cpp new file mode 100644 index 00000000..488a180a --- /dev/null +++ b/logic/minecraft/MinecraftVersion.cpp @@ -0,0 +1,143 @@ +#include "MinecraftVersion.h" +#include "InstanceVersion.h" +#include "VersionBuildError.h" +#include "VersionBuilder.h" + +bool MinecraftVersion::usesLegacyLauncher() +{ + return m_traits.contains("legacyLaunch") || m_traits.contains("aplhaLaunch"); +} + +QString MinecraftVersion::descriptor() +{ + return m_descriptor; +} + +QString MinecraftVersion::name() +{ + return m_name; +} + +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(); + } +} + +bool MinecraftVersion::hasJarMods() +{ + return false; +} + +bool MinecraftVersion::isMinecraftVersion() +{ + return true; +} + +// 1. assume the local file is good. load, check. If it's good, apply. +// 2. if discrepancies are found, fall out and fail (impossible to apply incomplete version). +void MinecraftVersion::applyFileTo(InstanceVersion *version) +{ + QFileInfo versionFile(QString("versions/%1/%1.dat").arg(m_descriptor)); + + auto versionObj = VersionBuilder::parseBinaryJsonFile(versionFile); + versionObj->applyTo(version); +} + +void MinecraftVersion::applyTo(InstanceVersion *version) +{ + // do we have this one cached? + if (m_versionSource == Local) + { + applyFileTo(version); + return; + } + // if not builtin, do not proceed any further. + if (m_versionSource != Builtin) + { + throw VersionIncomplete(QObject::tr( + "Minecraft version %1 could not be applied: version files are missing.").arg(m_descriptor)); + } + if (!m_descriptor.isNull()) + { + version->id = m_descriptor; + } + if (!m_mainClass.isNull()) + { + version->mainClass = m_mainClass; + } + if (!m_appletClass.isNull()) + { + version->appletClass = m_appletClass; + } + if (!m_processArguments.isNull()) + { + version->vanillaProcessArguments = m_processArguments; + version->processArguments = m_processArguments; + } + if (!m_type.isNull()) + { + version->type = m_type; + } + if (!m_releaseTimeString.isNull()) + { + version->m_releaseTimeString = m_releaseTimeString; + version->m_releaseTime = m_releaseTime; + } + if (!m_updateTimeString.isNull()) + { + version->m_updateTimeString = m_updateTimeString; + version->m_updateTime = m_updateTime; + } + version->traits.unite(m_traits); +} + +int MinecraftVersion::getOrder() +{ + return order; +} + +void MinecraftVersion::setOrder(int order) +{ + this->order = order; +} + +QList<JarmodPtr> MinecraftVersion::getJarMods() +{ + return QList<JarmodPtr>(); +} + +QString MinecraftVersion::getPatchName() +{ + return "Minecraft"; +} +QString MinecraftVersion::getPatchVersion() +{ + return m_descriptor; +} +QString MinecraftVersion::getPatchID() +{ + return "net.minecraft"; +} +QString MinecraftVersion::getPatchFilename() +{ + return QString(); +} diff --git a/logic/minecraft/MinecraftVersion.h b/logic/minecraft/MinecraftVersion.h new file mode 100644 index 00000000..82073a97 --- /dev/null +++ b/logic/minecraft/MinecraftVersion.h @@ -0,0 +1,103 @@ +/* Copyright 2013 Andrew Okin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QStringList> +#include <QSet> +#include <QDateTime> + +#include "logic/BaseVersion.h" +#include "VersionPatch.h" +#include "VersionFile.h" +#include "VersionSource.h" + +class InstanceVersion; +class MinecraftVersion; +typedef std::shared_ptr<MinecraftVersion> MinecraftVersionPtr; + +class MinecraftVersion : public BaseVersion, public VersionPatch +{ +public: /* methods */ + 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(InstanceVersion *version) override; + virtual int getOrder(); + virtual void setOrder(int order); + virtual QList<JarmodPtr> getJarMods() override; + virtual QString getPatchID() override; + virtual QString getPatchVersion() override; + virtual QString getPatchName() override; + virtual QString getPatchFilename() override; + bool needsUpdate() + { + return m_versionSource == Remote; + } + bool hasUpdate() + { + return m_versionSource == Remote || (m_versionSource == Local && upstreamUpdate); + } + +private: /* methods */ + void applyFileTo(InstanceVersion *version); + +public: /* data */ + /// The URL that this version will be downloaded from. maybe. + QString download_url; + + VersionSource m_versionSource = Builtin; + + /// the human readable version name + QString m_name; + + /// the version ID. + QString m_descriptor; + + /// version traits. added by MultiMC + QSet<QString> m_traits; + + /// The main class this version uses (if any, can be empty). + QString m_mainClass; + + /// The applet class this version uses (if any, can be empty). + QString m_appletClass; + + /// The process arguments used by this version + QString m_processArguments; + + /// The type of this release + QString m_type; + + /// the time this version was actually released by Mojang, as string and as QDateTime + QString m_releaseTimeString; + QDateTime m_releaseTime; + + /// the time this version was last updated by Mojang, as string and as QDateTime + QString m_updateTimeString; + QDateTime m_updateTime; + + /// MD5 hash of the minecraft jar + QString m_jarChecksum; + + /// order of this file... default = -2 + int order = -2; + + /// an update available from Mojang + MinecraftVersionPtr upstreamUpdate; +}; diff --git a/logic/minecraft/MinecraftVersionList.cpp b/logic/minecraft/MinecraftVersionList.cpp new file mode 100644 index 00000000..3aa1ac01 --- /dev/null +++ b/logic/minecraft/MinecraftVersionList.cpp @@ -0,0 +1,602 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QtXml> +#include "logic/MMCJson.h" +#include <QtAlgorithms> +#include <QtNetwork> + +#include "MultiMC.h" +#include "MMCError.h" + +#include "MinecraftVersionList.h" +#include "logic/net/URLConstants.h" + +#include "ParseUtils.h" +#include "VersionBuilder.h" +#include <logic/VersionFilterData.h> +#include <pathutils.h> + +static const char * localVersionCache = "versions/versions.dat"; + +class ListLoadError : public MMCError +{ +public: + ListLoadError(QString cause) : MMCError(cause) {}; + virtual ~ListLoadError() noexcept + { + } +}; + +MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent) +{ + loadBuiltinList(); + 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->m_releaseTime > right->m_releaseTime; +} + +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? + QLOG_ERROR() << "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.")); + } + loadMojangList(jsonDoc, Local); + } + catch (MMCError &e) + { + // the cache has gone bad for some reason... flush it. + QLOG_ERROR() << "The minecraft version cache is corrupted. Flushing cache."; + localIndex.remove(); + return; + } + m_hasLocalIndex = true; +} + +void MinecraftVersionList::loadBuiltinList() +{ + QLOG_INFO() << "Loading builtin version list."; + // grab the version list data from internal resources. + QResource versionList(":/versions/minecraft.json"); + QFile filez(versionList.absoluteFilePath()); + filez.open(QIODevice::ReadOnly); + auto data = filez.readAll(); + + // parse the data as json + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + QJsonObject root = jsonDoc.object(); + + // parse all the versions + for (const auto version : MMCJson::ensureArray(root.value("versions"))) + { + QJsonObject versionObj = version.toObject(); + QString versionID = versionObj.value("id").toString(""); + QString versionTypeStr = versionObj.value("type").toString(""); + if (versionID.isEmpty() || versionTypeStr.isEmpty()) + { + QLOG_ERROR() << "Parsed version is missing ID or type"; + continue; + } + + if (g_VersionFilterData.legacyBlacklist.contains(versionID)) + { + QLOG_WARN() << "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_name = mcVersion->m_descriptor = versionID; + + // Parse the timestamp. + if (!parse_timestamp(versionObj.value("releaseTime").toString(""), + mcVersion->m_releaseTimeString, mcVersion->m_releaseTime)) + { + QLOG_ERROR() << "Error while parsing version" << versionID + << ": invalid version timestamp"; + continue; + } + + // Get the download URL. + mcVersion->download_url = + "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + versionID + "/"; + + mcVersion->m_versionSource = Builtin; + mcVersion->m_type = versionTypeStr; + mcVersion->m_appletClass = versionObj.value("appletClass").toString(""); + mcVersion->m_mainClass = versionObj.value("mainClass").toString(""); + mcVersion->m_jarChecksum = versionObj.value("checksum").toString(""); + mcVersion->m_processArguments = versionObj.value("processArguments").toString("legacy"); + if (versionObj.contains("+traits")) + { + for (auto traitVal : MMCJson::ensureArray(versionObj.value("+traits"))) + { + mcVersion->m_traits.insert(MMCJson::ensureString(traitVal)); + } + } + m_lookup[versionID] = mcVersion; + m_vlist.append(mcVersion); + } +} + +void MinecraftVersionList::loadMojangList(QJsonDocument jsonDoc, VersionSource source) +{ + QLOG_INFO() << "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 = MMCJson::ensureObject(root.value("latest")); + m_latestReleaseID = MMCJson::ensureString(latest.value("release")); + m_latestSnapshotID = MMCJson::ensureString(latest.value("snapshot")); + } + catch (MMCError &err) + { + QLOG_ERROR() + << 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()) + { + QLOG_ERROR() << "Error while parsing version list : invalid JSON structure"; + continue; + } + + QJsonObject versionObj = version.toObject(); + QString versionID = versionObj.value("id").toString(""); + if (versionID.isEmpty()) + { + QLOG_ERROR() << "Error while parsing version : version ID is missing"; + continue; + } + + if (g_VersionFilterData.legacyBlacklist.contains(versionID)) + { + QLOG_WARN() << "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_name = mcVersion->m_descriptor = versionID; + + if (!parse_timestamp(versionObj.value("releaseTime").toString(""), + mcVersion->m_releaseTimeString, mcVersion->m_releaseTime)) + { + QLOG_ERROR() << "Error while parsing version" << versionID + << ": invalid release timestamp"; + continue; + } + if (!parse_timestamp(versionObj.value("time").toString(""), + mcVersion->m_updateTimeString, mcVersion->m_updateTime)) + { + QLOG_ERROR() << "Error while parsing version" << versionID + << ": invalid update timestamp"; + continue; + } + + if (mcVersion->m_releaseTime < g_VersionFilterData.legacyCutoffDate) + { + continue; + } + + // depends on where we load the version from -- network request or local file? + mcVersion->m_versionSource = source; + + QString dlUrl = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + versionID + "/"; + mcVersion->download_url = dlUrl; + QString versionTypeStr = versionObj.value("type").toString(""); + if (versionTypeStr.isEmpty()) + { + // FIXME: log this somewhere + 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 + { + // FIXME: log this somewhere + continue; + } + mcVersion->m_type = versionTypeStr; + tempList.append(mcVersion); + } + updateListData(tempList); + if(source == Remote) + { + m_loaded = true; + } +} + +void MinecraftVersionList::sort() +{ + beginResetModel(); + sortInternal(); + endResetModel(); +} + +BaseVersionPtr MinecraftVersionList::getLatestStable() const +{ + if(m_lookup.contains(m_latestReleaseID)) + return m_lookup[m_latestReleaseID]; + return BaseVersionPtr(); +} + +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; + } + // is it actually an update? + if (orig->m_updateTime >= added->m_updateTime) + { + // nope. + continue; + } + // alright, it's an update. put it inside the original, for further processing. + orig->upstreamUpdate = added; + } + sortInternal(); + endResetModel(); +} + +inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname) +{ + QDomNodeList elementList = parent.elementsByTagName(tagname); + if (elementList.count()) + return elementList.at(0).toElement(); + else + return QDomElement(); +} + +MCVListLoadTask::MCVListLoadTask(MinecraftVersionList *vlist) +{ + m_list = vlist; + m_currentStable = NULL; + vlistReply = nullptr; +} + +void MCVListLoadTask::executeTask() +{ + setStatus(tr("Loading instance version list...")); + auto worker = MMC->qnam(); + vlistReply = worker->get(QNetworkRequest( + QUrl("http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + "versions.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->loadMojangList(jsonDoc, Remote); + } + catch (MMCError &e) + { + emitFailed(e.cause()); + return; + } + + emitSucceeded(); + return; +} + +MCVListVersionUpdateTask::MCVListVersionUpdateTask(MinecraftVersionList *vlist, + QString updatedVersion) + : Task() +{ + m_list = vlist; + versionToUpdate = updatedVersion; +} + +void MCVListVersionUpdateTask::executeTask() +{ + QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + versionToUpdate + "/" + + versionToUpdate + ".json"; + auto job = new NetJob("Version index"); + job->addNetAction(ByteArrayDownload::make(QUrl(urlstr))); + 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 = VersionFile::fromJson(jsonDoc, "net.minecraft.json", false); + } + catch (MMCError &e) + { + emitFailed(tr("Couldn't process version file: %1").arg(e.cause())); + return; + } + QList<RawLibraryPtr> filteredLibs; + QList<RawLibraryPtr> lwjglLibs; + QSet<QString> lwjglFilter = { + "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"}; + for (auto lib : file->overwriteLibs) + { + if (lwjglFilter.contains(lib->fullname())) + { + lwjglLibs.append(lib); + } + else + { + filteredLibs.append(lib); + } + } + file->overwriteLibs = filteredLibs; + + // TODO: recognize and add LWJGL versions here. + + file->fileId = "net.minecraft"; + + // now dump the file to disk + auto doc = file->toJson(false); + auto newdata = doc.toBinaryData(); + QLOG_INFO() << newdata; + QString targetPath = "versions/" + versionToUpdate + "/" + versionToUpdate + ".dat"; + 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(versionToUpdate); + emitSucceeded(); +} + +std::shared_ptr<Task> MinecraftVersionList::createUpdateTask(QString version) +{ + return std::shared_ptr<Task>(new MCVListVersionUpdateTask(this, version)); +} + +void MinecraftVersionList::saveCachedList() +{ + // FIXME: throw. + if (!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("time", mcversion->m_updateTimeString); + entryObj.insert("releaseTime", mcversion->m_releaseTimeString); + 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 (updatedVersion->m_versionSource == Builtin) + return; + + if (updatedVersion->upstreamUpdate) + { + auto updatedWith = updatedVersion->upstreamUpdate; + updatedWith->m_versionSource = Local; + m_vlist[idx] = updatedWith; + m_lookup[version] = updatedWith; + } + else + { + updatedVersion->m_versionSource = Local; + } + + dataChanged(index(idx), index(idx)); + + saveCachedList(); +} diff --git a/logic/minecraft/MinecraftVersionList.h b/logic/minecraft/MinecraftVersionList.h new file mode 100644 index 00000000..4753ce05 --- /dev/null +++ b/logic/minecraft/MinecraftVersionList.h @@ -0,0 +1,108 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QObject> +#include <QList> +#include <QSet> + +#include "logic/BaseVersionList.h" +#include "logic/tasks/Task.h" +#include "logic/minecraft/MinecraftVersion.h" +#include <logic/net/NetJob.h> + +class MCVListLoadTask; +class MCVListVersionUpdateTask; +class QNetworkReply; + +class MinecraftVersionList : public BaseVersionList +{ + Q_OBJECT +private: + void sortInternal(); + void loadBuiltinList(); + void loadMojangList(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(); + virtual bool isLoaded(); + virtual const BaseVersionPtr at(int i) const; + virtual int count() const; + virtual void sort(); + + virtual BaseVersionPtr getLatestStable() const; + +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); +}; + +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, QString updatedVersion); + virtual ~MCVListVersionUpdateTask() override{}; + virtual void executeTask() override; + +protected +slots: + void json_downloaded(); + +protected: + NetJobPtr specificVersionDownloadJob; + QString versionToUpdate; + MinecraftVersionList *m_list; +}; diff --git a/logic/minecraft/OneSixLibrary.cpp b/logic/minecraft/OneSixLibrary.cpp new file mode 100644 index 00000000..7f69d9f8 --- /dev/null +++ b/logic/minecraft/OneSixLibrary.cpp @@ -0,0 +1,233 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QJsonArray> + +#include "OneSixLibrary.h" +#include "OneSixRule.h" +#include "OpSys.h" +#include "logic/net/URLConstants.h" +#include <pathutils.h> +#include <JlCompress.h> +#include "logger/QsLog.h" + +OneSixLibrary::OneSixLibrary(RawLibraryPtr base) +{ + m_name = base->m_name; + m_base_url = base->m_base_url; + m_hint = base->m_hint; + m_absolute_url = base->m_absolute_url; + extract_excludes = base->extract_excludes; + m_native_suffixes = base->m_native_suffixes; + m_rules = base->m_rules; + finalize(); +} + +OneSixLibraryPtr OneSixLibrary::fromRawLibrary(RawLibraryPtr lib) +{ + return OneSixLibraryPtr(new OneSixLibrary(lib)); +} + +void OneSixLibrary::finalize() +{ + QStringList parts = m_name.split(':'); + QString relative = parts[0]; + relative.replace('.', '/'); + relative += '/' + parts[1] + '/' + parts[2] + '/' + parts[1] + '-' + parts[2]; + + if (!isNative()) + relative += ".jar"; + else + { + if (m_native_suffixes.contains(currentSystem)) + { + relative += "-" + m_native_suffixes[currentSystem] + ".jar"; + } + else + { + // really, bad. + relative += ".jar"; + } + } + + m_decentname = parts[1]; + m_decentversion = minVersion = parts[2]; + m_storage_path = relative; + m_download_url = m_base_url + relative; + + if (m_rules.empty()) + { + m_is_active = true; + } + else + { + RuleAction result = Disallow; + for (auto rule : m_rules) + { + RuleAction temp = rule->apply(this); + if (temp != Defer) + result = temp; + } + m_is_active = (result == Allow); + } + if (isNative()) + { + m_is_active = m_is_active && m_native_suffixes.contains(currentSystem); + m_decenttype = "Native"; + } + else + { + m_decenttype = "Java"; + } +} + +void OneSixLibrary::setName(const QString &name) +{ + m_name = name; +} +void OneSixLibrary::setBaseUrl(const QString &base_url) +{ + m_base_url = base_url; +} +void OneSixLibrary::addNative(OpSys os, const QString &suffix) +{ + m_native_suffixes[os] = suffix; +} +void OneSixLibrary::clearSuffixes() +{ + m_native_suffixes.clear(); +} +void OneSixLibrary::setRules(QList<std::shared_ptr<Rule>> rules) +{ + m_rules = rules; +} +bool OneSixLibrary::isActive() const +{ + return m_is_active; +} +QString OneSixLibrary::downloadUrl() const +{ + if (m_absolute_url.size()) + return m_absolute_url; + return m_download_url; +} +QString OneSixLibrary::storagePath() const +{ + return m_storage_path; +} + +void OneSixLibrary::setAbsoluteUrl(const QString &absolute_url) +{ + m_absolute_url = absolute_url; +} + +QString OneSixLibrary::absoluteUrl() const +{ + return m_absolute_url; +} + +void OneSixLibrary::setHint(const QString &hint) +{ + m_hint = hint; +} + +QString OneSixLibrary::hint() const +{ + return m_hint; +} + +QStringList OneSixLibrary::files() +{ + QStringList retval; + QString storage = storagePath(); + if (storage.contains("${arch}")) + { + QString cooked_storage = storage; + cooked_storage.replace("${arch}", "32"); + retval.append(cooked_storage); + cooked_storage = storage; + cooked_storage.replace("${arch}", "64"); + retval.append(cooked_storage); + } + else + retval.append(storage); + return retval; +} + +bool OneSixLibrary::filesExist(const QDir &base) +{ + auto libFiles = files(); + for(auto file: libFiles) + { + QFileInfo info(base, file); + QLOG_WARN() << info.absoluteFilePath() << "doesn't exist"; + if (!info.exists()) + return false; + } + return true; +} + +bool OneSixLibrary::extractTo(QString target_dir) +{ + QString storage = storagePath(); + if (storage.contains("${arch}")) + { + QString cooked_storage = storage; + cooked_storage.replace("${arch}", "32"); + QString origin = PathCombine("libraries", cooked_storage); + QString target_dir_cooked = PathCombine(target_dir, "32"); + if (!ensureFolderPathExists(target_dir_cooked)) + { + QLOG_ERROR() << "Couldn't create folder " + target_dir_cooked; + return false; + } + if (JlCompress::extractWithExceptions(origin, target_dir_cooked, extract_excludes) + .isEmpty()) + { + QLOG_ERROR() << "Couldn't extract " + origin; + return false; + } + cooked_storage = storage; + cooked_storage.replace("${arch}", "64"); + origin = PathCombine("libraries", cooked_storage); + target_dir_cooked = PathCombine(target_dir, "64"); + if (!ensureFolderPathExists(target_dir_cooked)) + { + QLOG_ERROR() << "Couldn't create folder " + target_dir_cooked; + return false; + } + if (JlCompress::extractWithExceptions(origin, target_dir_cooked, extract_excludes) + .isEmpty()) + { + QLOG_ERROR() << "Couldn't extract " + origin; + return false; + } + } + else + { + if (!ensureFolderPathExists(target_dir)) + { + QLOG_ERROR() << "Couldn't create folder " + target_dir; + return false; + } + QString path = PathCombine("libraries", storage); + if (JlCompress::extractWithExceptions(path, target_dir, extract_excludes).isEmpty()) + { + QLOG_ERROR() << "Couldn't extract " + path; + return false; + } + } + return true; +} diff --git a/logic/minecraft/OneSixLibrary.h b/logic/minecraft/OneSixLibrary.h new file mode 100644 index 00000000..3d38985b --- /dev/null +++ b/logic/minecraft/OneSixLibrary.h @@ -0,0 +1,130 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QString> +#include <QStringList> +#include <QMap> +#include <QJsonObject> +#include <QDir> +#include <memory> + +#include "logic/net/URLConstants.h" +#include "logic/minecraft/OpSys.h" +#include "logic/minecraft/RawLibrary.h" + +class Rule; + +class OneSixLibrary; +typedef std::shared_ptr<OneSixLibrary> OneSixLibraryPtr; + +class OneSixLibrary : public RawLibrary +{ +private: + /// a decent name fit for display + QString m_decentname; + /// a decent version fit for display + QString m_decentversion; + /// a decent type fit for display + QString m_decenttype; + /// where to store the lib locally + QString m_storage_path; + /// where to download the lib from + QString m_download_url; + /// is this lib actually active on the current OS? + bool m_is_active = false; + +public: + + QString minVersion; + + enum DependType + { + Soft, + Hard + }; + DependType dependType; + +public: + /// Constructor + OneSixLibrary(const QString &name, const DependType type = Soft) + { + m_name = name; + dependType = type; + } + /// Constructor + OneSixLibrary(RawLibraryPtr base); + static OneSixLibraryPtr fromRawLibrary(RawLibraryPtr lib); + + /// Returns the raw name field + QString rawName() const + { + return m_name; + } + + /** + * finalize the library, processing the input values into derived values and state + * + * This SHALL be called after all the values are parsed or after any further change. + */ + void finalize(); + + /// Set the library composite name + void setName(const QString &name); + /// get a decent-looking name + QString name() const + { + return m_decentname; + } + /// get a decent-looking version + QString version() const + { + return m_decentversion; + } + /// what kind of library is it? (for display) + QString type() const + { + return m_decenttype; + } + /// Set the url base for downloads + void setBaseUrl(const QString &base_url); + + /// Attach a name suffix to the specified OS native + void addNative(OpSys os, const QString &suffix); + /// Clears all suffixes + void clearSuffixes(); + /// Set the load rules + void setRules(QList<std::shared_ptr<Rule>> rules); + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive() const; + /// Get the URL to download the library from + QString downloadUrl() const; + /// Get the relative path where the library should be saved + QString storagePath() const; + + /// set an absolute URL for the library. This is an MMC extension. + void setAbsoluteUrl(const QString &absolute_url); + QString absoluteUrl() const; + + /// set a hint about how to treat the library. This is an MMC extension. + void setHint(const QString &hint); + QString hint() const; + + bool extractTo(QString target_dir); + bool filesExist(const QDir &base); + QStringList files(); +}; diff --git a/logic/minecraft/OneSixRule.cpp b/logic/minecraft/OneSixRule.cpp new file mode 100644 index 00000000..93c49e5e --- /dev/null +++ b/logic/minecraft/OneSixRule.cpp @@ -0,0 +1,90 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QJsonObject> +#include <QJsonArray> + +#include "OneSixRule.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)); + osObj.insert("version", m_version_regexp); + } + ruleObj.insert("os", osObj); + return ruleObj; +} + diff --git a/logic/minecraft/OneSixRule.h b/logic/minecraft/OneSixRule.h new file mode 100644 index 00000000..a18093b0 --- /dev/null +++ b/logic/minecraft/OneSixRule.h @@ -0,0 +1,102 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QString> +#include <QList> +#include <QJsonObject> +#include <memory> +#include "OpSys.h" + +class OneSixLibrary; +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(OneSixLibrary *parent) = 0; + +public: + Rule(RuleAction result) : m_result(result) + { + } + virtual ~Rule() {}; + virtual QJsonObject toJson() = 0; + RuleAction apply(OneSixLibrary *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(OneSixLibrary *) + { + 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(OneSixLibrary *) + { + 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/logic/minecraft/OpSys.cpp b/logic/minecraft/OpSys.cpp new file mode 100644 index 00000000..e001b7f3 --- /dev/null +++ b/logic/minecraft/OpSys.cpp @@ -0,0 +1,42 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "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/logic/minecraft/OpSys.h b/logic/minecraft/OpSys.h new file mode 100644 index 00000000..363c87d7 --- /dev/null +++ b/logic/minecraft/OpSys.h @@ -0,0 +1,37 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include <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/logic/minecraft/ParseUtils.cpp b/logic/minecraft/ParseUtils.cpp new file mode 100644 index 00000000..f94de6ff --- /dev/null +++ b/logic/minecraft/ParseUtils.cpp @@ -0,0 +1,24 @@ +#include <QDateTime> +#include <QString> +#include "ParseUtils.h" +#include <logic/MMCJson.h> + +QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + +bool parse_timestamp (const QString & raw, QString & save_here, QDateTime & parse_here) +{ + save_here = raw; + if (save_here.isEmpty()) + { + return false; + } + parse_here = timeFromS3Time(save_here); + if (!parse_here.isValid()) + { + return false; + } + return true; +} diff --git a/logic/minecraft/ParseUtils.h b/logic/minecraft/ParseUtils.h new file mode 100644 index 00000000..bd2f6ffb --- /dev/null +++ b/logic/minecraft/ParseUtils.h @@ -0,0 +1,14 @@ +#pragma once +#include <QString> +#include <QDateTime> + +/** + * parse the S3 timestamp in 'raw' and fill the forwarded variables. + * return true/false for success/failure + */ +bool parse_timestamp (const QString &raw, QString &save_here, QDateTime &parse_here); + +/** + * take the timestamp used by S3 and turn it into QDateTime + */ +QDateTime timeFromS3Time(QString str); diff --git a/logic/minecraft/RawLibrary.cpp b/logic/minecraft/RawLibrary.cpp new file mode 100644 index 00000000..7e0ebff0 --- /dev/null +++ b/logic/minecraft/RawLibrary.cpp @@ -0,0 +1,205 @@ +#include "logic/MMCJson.h" +using namespace MMCJson; + +#include "RawLibrary.h" + +RawLibraryPtr RawLibrary::fromJson(const QJsonObject &libObj, const QString &filename) +{ + RawLibraryPtr out(new RawLibrary()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + + "contains a library that doesn't have a 'name' field"); + } + out->m_name = libObj.value("name").toString(); + + auto readString = [libObj, filename](const QString & key, QString & variable) + { + if (libObj.contains(key)) + { + QJsonValue val = libObj.value(key); + if (!val.isString()) + { + QLOG_WARN() << key << "is not a string in" << filename << "(skipping)"; + } + else + { + variable = val.toString(); + } + } + }; + + readString("url", out->m_base_url); + readString("MMC-hint", out->m_hint); + readString("MMC-absulute_url", out->m_absolute_url); + readString("MMC-absoluteUrl", out->m_absolute_url); + if (libObj.contains("extract")) + { + out->applyExcludes = true; + auto extractObj = ensureObject(libObj.value("extract")); + for (auto excludeVal : ensureArray(extractObj.value("exclude"))) + { + out->extract_excludes.append(ensureString(excludeVal)); + } + } + if (libObj.contains("natives")) + { + QJsonObject nativesObj = ensureObject(libObj.value("natives")); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + QLOG_WARN() << filename << "contains an invalid native (skipping)"; + } + OpSys opSys = OpSys_fromString(it.key()); + if (opSys != Os_Other) + { + out->m_native_suffixes[opSys] = it.value().toString(); + } + } + } + if (libObj.contains("rules")) + { + out->applyRules = true; + out->m_rules = rulesFromJsonV4(libObj); + } + return out; +} + +RawLibraryPtr RawLibrary::fromJsonPlus(const QJsonObject &libObj, const QString &filename) +{ + auto lib = RawLibrary::fromJson(libObj, filename); + if (libObj.contains("insert")) + { + QJsonValue insertVal = ensureExists(libObj.value("insert"), "library insert rule"); + QString insertString; + { + if (insertVal.isString()) + { + insertString = insertVal.toString(); + } + else if (insertVal.isObject()) + { + QJsonObject insertObj = insertVal.toObject(); + if (insertObj.isEmpty()) + { + throw JSONValidationError("One library has an empty insert object in " + + filename); + } + insertString = insertObj.keys().first(); + lib->insertData = insertObj.value(insertString).toString(); + } + } + if (insertString == "apply") + { + lib->insertType = RawLibrary::Apply; + } + else if (insertString == "prepend") + { + lib->insertType = RawLibrary::Prepend; + } + else if (insertString == "append") + { + lib->insertType = RawLibrary::Append; + } + else if (insertString == "replace") + { + lib->insertType = RawLibrary::Replace; + } + else + { + throw JSONValidationError("A '+' library in " + filename + + " contains an invalid insert type"); + } + } + if (libObj.contains("MMC-depend")) + { + const QString dependString = ensureString(libObj.value("MMC-depend")); + if (dependString == "hard") + { + lib->dependType = RawLibrary::Hard; + } + else if (dependString == "soft") + { + lib->dependType = RawLibrary::Soft; + } + else + { + throw JSONValidationError("A '+' library in " + filename + + " contains an invalid depend type"); + } + } + return lib; +} + +bool RawLibrary::isNative() const +{ + return m_native_suffixes.size() != 0; +} + +QJsonObject RawLibrary::toJson() +{ + QJsonObject libRoot; + libRoot.insert("name", m_name); + if (m_absolute_url.size()) + libRoot.insert("MMC-absoluteUrl", m_absolute_url); + if (m_hint.size()) + libRoot.insert("MMC-hint", m_hint); + if (m_base_url != "http://" + URLConstants::AWS_DOWNLOAD_LIBRARIES && + m_base_url != "https://" + URLConstants::AWS_DOWNLOAD_LIBRARIES && + m_base_url != "https://" + URLConstants::LIBRARY_BASE && !m_base_url.isEmpty()) + { + libRoot.insert("url", m_base_url); + } + if (isNative()) + { + QJsonObject nativeList; + auto iter = m_native_suffixes.begin(); + while (iter != m_native_suffixes.end()) + { + nativeList.insert(OpSys_toString(iter.key()), iter.value()); + iter++; + } + libRoot.insert("natives", nativeList); + if (extract_excludes.size()) + { + QJsonArray excludes; + QJsonObject extract; + for (auto exclude : extract_excludes) + { + excludes.append(exclude); + } + extract.insert("exclude", excludes); + libRoot.insert("extract", extract); + } + } + if (m_rules.size()) + { + QJsonArray allRules; + for (auto &rule : m_rules) + { + QJsonObject ruleObj = rule->toJson(); + allRules.append(ruleObj); + } + libRoot.insert("rules", allRules); + } + return libRoot; +} + +QString RawLibrary::fullname() +{ + QStringList parts = m_name.split(':'); + return parts[0] + ":" + parts[1]; +} + +QString RawLibrary::group() +{ + QStringList parts = m_name.split(':'); + return parts[0]; +} + +QString RawLibrary::version() +{ + QStringList parts = m_name.split(':'); + return parts[2]; +} diff --git a/logic/minecraft/RawLibrary.h b/logic/minecraft/RawLibrary.h new file mode 100644 index 00000000..f5a28c61 --- /dev/null +++ b/logic/minecraft/RawLibrary.h @@ -0,0 +1,64 @@ +#pragma once +#include <QString> +#include <QPair> +#include <QList> +#include <QStringList> +#include <QMap> +#include <memory> + +#include "logic/minecraft/OneSixRule.h" +#include "logic/minecraft/OpSys.h" +#include "logic/net/URLConstants.h" + +class RawLibrary; +typedef std::shared_ptr<RawLibrary> RawLibraryPtr; + +class RawLibrary +{ +public: /* methods */ + /// read and create a basic library + static RawLibraryPtr fromJson(const QJsonObject &libObj, const QString &filename); + /// read and create a MultiMC '+' library. Those have some extra fields. + static RawLibraryPtr fromJsonPlus(const QJsonObject &libObj, const QString &filename); + QJsonObject toJson(); + + QString fullname(); + QString version(); + QString group(); + +public: /* data */ + QString m_name; + QString m_base_url = "https://" + URLConstants::LIBRARY_BASE; + /// type hint - modifies how the library is treated + QString m_hint; + /// absolute URL. takes precedence over m_download_path, if defined + QString m_absolute_url; + + bool applyExcludes = false; + QStringList extract_excludes; + + /// Returns true if the library is native + bool isNative() const; + /// native suffixes per OS + QMap<OpSys, QString> m_native_suffixes; + + bool applyRules = false; + QList<std::shared_ptr<Rule>> m_rules; + + // used for '+' libraries + enum InsertType + { + Apply, + Append, + Prepend, + Replace + } insertType = Append; + QString insertData; + + // soft or hard dependency? hard means 'needs equal', soft means 'needs equal or newer' + enum DependType + { + Soft, + Hard + } dependType = Soft; +}; diff --git a/logic/minecraft/VersionBuildError.h b/logic/minecraft/VersionBuildError.h new file mode 100644 index 00000000..ae479851 --- /dev/null +++ b/logic/minecraft/VersionBuildError.h @@ -0,0 +1,58 @@ +#include "MMCError.h" + +class VersionBuildError : public MMCError +{ +public: + VersionBuildError(QString cause) : MMCError(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 + { + } +};
\ No newline at end of file diff --git a/logic/minecraft/VersionBuilder.cpp b/logic/minecraft/VersionBuilder.cpp new file mode 100644 index 00000000..ddcf6333 --- /dev/null +++ b/logic/minecraft/VersionBuilder.cpp @@ -0,0 +1,349 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QList> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonDocument> +#include <QFile> +#include <QFileInfo> +#include <QMessageBox> +#include <QObject> +#include <QDir> +#include <QDebug> +#include <qresource.h> +#include <modutils.h> + +#include "MultiMC.h" +#include "logic/minecraft/VersionBuilder.h" +#include "logic/minecraft/InstanceVersion.h" +#include "logic/minecraft/OneSixRule.h" +#include "logic/minecraft/VersionPatch.h" +#include "logic/minecraft/VersionFile.h" +#include "VersionBuildError.h" +#include "MinecraftVersionList.h" + +#include "logic/OneSixInstance.h" +#include "logic/MMCJson.h" + +#include "logger/QsLog.h" + +VersionBuilder::VersionBuilder() +{ +} + +void VersionBuilder::build(InstanceVersion *version, OneSixInstance *instance, + const QStringList &external) +{ + VersionBuilder builder; + builder.m_version = version; + builder.m_instance = instance; + builder.external_patches = external; + builder.buildInternal(); +} + +void VersionBuilder::readJsonAndApplyToVersion(InstanceVersion *version, const QJsonObject &obj) +{ + VersionBuilder builder; + builder.m_version = version; + builder.m_instance = 0; + builder.readJsonAndApply(obj); +} + +void VersionBuilder::buildFromCustomJson() +{ + QLOG_INFO() << "Building version from custom.json within the instance."; + QLOG_INFO() << "Reading custom.json"; + auto file = parseJsonFile(QFileInfo(instance_root.absoluteFilePath("custom.json")), false); + file->name = "custom.json"; + file->filename = "custom.json"; + file->fileId = "org.multimc.custom.json"; + file->order = -1; + file->version = QString(); + m_version->VersionPatches.append(file); + m_version->finalize(); + return; +} + +void VersionBuilder::buildFromVersionJson() +{ + QLOG_INFO() << "Building version from version.json and patches within the instance."; + QLOG_INFO() << "Reading version.json"; + auto file = parseJsonFile(QFileInfo(instance_root.absoluteFilePath("version.json")), false); + file->name = "Minecraft"; + file->fileId = "org.multimc.version.json"; + file->order = -1; + file->version = m_instance->intendedVersionId(); + file->mcVersion = m_instance->intendedVersionId(); + m_version->VersionPatches.append(file); + + // load all patches, put into map for ordering, apply in the right order + readInstancePatches(); + + // some final touches + m_version->finalize(); +} + +void VersionBuilder::readInstancePatches() +{ + PatchOrder userOrder; + readOverrideOrders(m_instance, userOrder); + QDir patches(instance_root.absoluteFilePath("patches/")); + + // 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"); + QLOG_INFO() << "Reading" << filename << "by user order"; + auto file = parseJsonFile(QFileInfo(filename), false); + // sanity check. prevent tampering with files. + if (file->fileId != id) + { + throw VersionBuildError( + QObject::tr("load id %1 does not match internal id %2").arg(id, file->fileId)); + } + m_version->VersionPatches.append(file); + } + // now load the rest by internal preference. + QMap<int, QPair<QString, VersionFilePtr>> files; + for (auto info : patches.entryInfoList(QStringList() << "*.json", QDir::Files)) + { + // parse the file + QLOG_INFO() << "Reading" << info.fileName(); + auto file = parseJsonFile(info, true); + // ignore builtins + if (file->fileId == "net.minecraft") + continue; + if (file->fileId == "org.lwjgl") + continue; + // do not load what we already loaded in the first pass + if (userOrder.contains(file->fileId)) + continue; + if (files.contains(file->order)) + { + // FIXME: do not throw? + throw VersionBuildError(QObject::tr("%1 has the same order as %2") + .arg(file->fileId, files[file->order].second->fileId)); + } + files.insert(file->order, qMakePair(info.fileName(), file)); + } + for (auto order : files.keys()) + { + auto &filePair = files[order]; + m_version->VersionPatches.append(filePair.second); + } +} + +void VersionBuilder::buildFromExternalPatches() +{ + QLOG_INFO() << "Building version from external files."; + int externalOrder = -1; + for (auto fileName : external_patches) + { + QLOG_INFO() << "Reading" << fileName; + auto file = parseJsonFile(QFileInfo(fileName), false, fileName.endsWith("pack.json")); + file->name = QFileInfo(fileName).fileName(); + file->fileId = "org.multimc.external." + file->name; + file->order = (externalOrder += 1); + file->version = QString(); + file->mcVersion = QString(); + m_version->VersionPatches.append(file); + } + // some final touches + m_version->finalize(); +} + +void VersionBuilder::buildFromMultilayer() +{ + QLOG_INFO() << "Building version from multilayered sources."; + // just the builtin stuff for now + auto minecraftList = MMC->minecraftlist(); + auto mcversion = minecraftList->findVersion(m_instance->intendedVersionId()); + auto minecraftPatch = std::dynamic_pointer_cast<VersionPatch>(mcversion); + if (!minecraftPatch) + { + throw VersionIncomplete("net.minecraft"); + } + minecraftPatch->setOrder(-2); + m_version->VersionPatches.append(minecraftPatch); + + // TODO: this is obviously fake. + QResource LWJGL(":/versions/LWJGL/2.9.1.json"); + auto lwjgl = parseJsonFile(LWJGL.absoluteFilePath(), false, false); + auto lwjglPatch = std::dynamic_pointer_cast<VersionPatch>(lwjgl); + if (!lwjglPatch) + { + throw VersionIncomplete("org.lwjgl"); + } + lwjglPatch->setOrder(-1); + m_version->VersionPatches.append(lwjglPatch); + + // load all patches, put into map for ordering, apply in the right order + readInstancePatches(); + + m_version->finalize(); +} + +void VersionBuilder::buildInternal() +{ + m_version->VersionPatches.clear(); + instance_root = QDir(m_instance->instanceRoot()); + // if we do external files, do just those. + if (!external_patches.isEmpty()) + { + buildFromExternalPatches(); + } + // else, if there's custom json, we just do that. + else if (QFile::exists(instance_root.absoluteFilePath("custom.json"))) + { + buildFromCustomJson(); + } + // version.json -> patches/*.json + else if (QFile::exists(instance_root.absoluteFilePath("version.json"))) + { + buildFromVersionJson(); + } + else + { + buildFromMultilayer(); + } +} + +void VersionBuilder::readJsonAndApply(const QJsonObject &obj) +{ + m_version->clear(); + + auto file = VersionFile::fromJson(QJsonDocument(obj), QString(), false); + + file->applyTo(m_version); + m_version->VersionPatches.append(file); +} + +VersionFilePtr VersionBuilder::parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder, + bool isFTB) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + throw JSONValidationError(QObject::tr("Unable to open the version file %1: %2.") + .arg(fileInfo.fileName(), file.errorString())); + } + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + throw JSONValidationError( + QObject::tr("Unable to process the version file %1: %2 at %3.") + .arg(fileInfo.fileName(), error.errorString()) + .arg(error.offset)); + } + return VersionFile::fromJson(doc, file.fileName(), requireOrder, isFTB); +} + +VersionFilePtr VersionBuilder::parseBinaryJsonFile(const QFileInfo &fileInfo) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + throw JSONValidationError(QObject::tr("Unable to open the version file %1: %2.") + .arg(fileInfo.fileName(), file.errorString())); + } + 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 VersionFile::fromJson(doc, file.fileName(), false, false); +} + +static const int currentOrderFileVersion = 1; + +bool VersionBuilder::readOverrideOrders(OneSixInstance *instance, PatchOrder &order) +{ + QFile orderFile(instance->instanceRoot() + "/order.json"); + if (!orderFile.open(QFile::ReadOnly)) + { + QLOG_ERROR() << "Couldn't open" << orderFile.fileName() + << " for reading:" << orderFile.errorString(); + QLOG_WARN() << "Ignoring overriden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + QLOG_ERROR() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + QLOG_WARN() << "Ignoring overriden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = MMCJson::ensureObject(doc); + // check order file version. + auto version = MMCJson::ensureInteger(obj.value("version"), "version"); + if (version != currentOrderFileVersion) + { + throw JSONValidationError(QObject::tr("Invalid order file version, expected %1") + .arg(currentOrderFileVersion)); + } + auto orderArray = MMCJson::ensureArray(obj.value("order")); + for(auto item: orderArray) + { + order.append(MMCJson::ensureString(item)); + } + } + catch (JSONValidationError &err) + { + QLOG_ERROR() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; + QLOG_WARN() << "Ignoring overriden order"; + order.clear(); + return false; + } + return true; +} + +bool VersionBuilder::writeOverrideOrders(OneSixInstance *instance, const PatchOrder &order) +{ + QJsonObject obj; + obj.insert("version", currentOrderFileVersion); + QJsonArray orderArray; + for(auto str: order) + { + orderArray.append(str); + } + obj.insert("order", orderArray); + QFile orderFile(instance->instanceRoot() + "/order.json"); + if (!orderFile.open(QFile::WriteOnly)) + { + QLOG_ERROR() << "Couldn't open" << orderFile.fileName() + << "for writing:" << orderFile.errorString(); + return false; + } + orderFile.write(QJsonDocument(obj).toJson(QJsonDocument::Indented)); + return true; +} diff --git a/logic/minecraft/VersionBuilder.h b/logic/minecraft/VersionBuilder.h new file mode 100644 index 00000000..350179b9 --- /dev/null +++ b/logic/minecraft/VersionBuilder.h @@ -0,0 +1,56 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QString> +#include <QMap> +#include "VersionFile.h" + +class InstanceVersion; +class OneSixInstance; +class QJsonObject; +class QFileInfo; + +typedef QStringList PatchOrder; + +class VersionBuilder +{ + VersionBuilder(); +public: + static void build(InstanceVersion *version, OneSixInstance *instance, const QStringList &external); + static void readJsonAndApplyToVersion(InstanceVersion *version, const QJsonObject &obj); + static VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder, bool isFTB = false); + static VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo); + + bool readOverrideOrders(OneSixInstance *instance, PatchOrder &order); + static bool writeOverrideOrders(OneSixInstance *instance, const PatchOrder &order); + +private: + InstanceVersion *m_version; + OneSixInstance *m_instance; + QStringList external_patches; + QDir instance_root; + + void buildInternal(); + void buildFromExternalPatches(); + void buildFromCustomJson(); + void buildFromVersionJson(); + void buildFromMultilayer(); + + void readInstancePatches(); + + void readJsonAndApply(const QJsonObject &obj); +}; diff --git a/logic/minecraft/VersionFile.cpp b/logic/minecraft/VersionFile.cpp new file mode 100644 index 00000000..93f57116 --- /dev/null +++ b/logic/minecraft/VersionFile.cpp @@ -0,0 +1,535 @@ +#include <QJsonArray> +#include <QJsonDocument> +#include <modutils.h> + +#include "logger/QsLog.h" + +#include "logic/minecraft/VersionFile.h" +#include "logic/minecraft/OneSixLibrary.h" +#include "logic/minecraft/InstanceVersion.h" +#include "logic/minecraft/JarMod.h" +#include "ParseUtils.h" + +#include "logic/MMCJson.h" +using namespace MMCJson; + +#include "VersionBuildError.h" + +#define CURRENT_MINIMUM_LAUNCHER_VERSION 14 + +int findLibrary(QList<OneSixLibraryPtr> haystack, const QString &needle) +{ + int retval = -1; + for (int i = 0; i < haystack.size(); ++i) + { + QString chunk = haystack.at(i)->rawName(); + if (QRegExp(needle, Qt::CaseSensitive, QRegExp::WildcardUnix).indexIn(chunk) != -1) + { + // only one is allowed. + if (retval != -1) + return -1; + retval = i; + } + } + return retval; +} + +VersionFilePtr VersionFile::fromJson(const QJsonDocument &doc, const QString &filename, + const bool requireOrder, const bool isFTB) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + } + + QJsonObject root = doc.object(); + + if (requireOrder) + { + if (root.contains("order")) + { + out->order = ensureInteger(root.value("order")); + } + else + { + // FIXME: evaluate if we don't want to throw exceptions here instead + QLOG_ERROR() << 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->mcVersion = root.value("mcVersion").toString(); + out->filename = filename; + + auto readString = [root](const QString & key, QString & variable) + { + if (root.contains(key)) + { + variable = ensureString(root.value(key)); + } + }; + + auto readStringRet = [root](const QString & key)->QString + { + if (root.contains(key)) + { + return ensureString(root.value(key)); + } + return QString(); + } + ; + + // FIXME: This should be ignored when applying. + if (!isFTB) + { + readString("id", out->id); + } + + readString("mainClass", out->mainClass); + readString("appletClass", out->appletClass); + readString("processArguments", out->processArguments); + readString("minecraftArguments", out->overwriteMinecraftArguments); + readString("+minecraftArguments", out->addMinecraftArguments); + readString("-minecraftArguments", out->removeMinecraftArguments); + readString("type", out->type); + + parse_timestamp(readStringRet("releaseTime"), out->m_releaseTimeString, out->m_releaseTime); + parse_timestamp(readStringRet("time"), out->m_updateTimeString, out->m_updateTime); + + readString("assets", out->assets); + + if (root.contains("minimumLauncherVersion")) + { + out->minimumLauncherVersion = ensureInteger(root.value("minimumLauncherVersion")); + } + + if (root.contains("tweakers")) + { + out->shouldOverwriteTweakers = true; + for (auto tweakerVal : ensureArray(root.value("tweakers"))) + { + out->overwriteTweakers.append(ensureString(tweakerVal)); + } + } + + if (root.contains("+tweakers")) + { + for (auto tweakerVal : ensureArray(root.value("+tweakers"))) + { + out->addTweakers.append(ensureString(tweakerVal)); + } + } + + if (root.contains("-tweakers")) + { + for (auto tweakerVal : ensureArray(root.value("-tweakers"))) + { + out->removeTweakers.append(ensureString(tweakerVal)); + } + } + + if (root.contains("+traits")) + { + for (auto tweakerVal : ensureArray(root.value("+traits"))) + { + out->traits.insert(ensureString(tweakerVal)); + } + } + + if (root.contains("libraries")) + { + // FIXME: This should be done when applying. + out->shouldOverwriteLibs = !isFTB; + for (auto libVal : ensureArray(root.value("libraries"))) + { + auto libObj = ensureObject(libVal); + + auto lib = RawLibrary::fromJson(libObj, filename); + // FIXME: This should be done when applying. + if (isFTB) + { + lib->m_hint = "local"; + lib->insertType = RawLibrary::Prepend; + out->addLibs.prepend(lib); + } + else + { + out->overwriteLibs.append(lib); + } + } + } + + if (root.contains("+jarMods")) + { + for (auto libVal : ensureArray(root.value("+jarMods"))) + { + QJsonObject libObj = ensureObject(libVal); + // parse the jarmod + auto lib = Jarmod::fromJson(libObj, filename); + // and add to jar mods + out->jarMods.append(lib); + } + } + + if (root.contains("+libraries")) + { + for (auto libVal : ensureArray(root.value("+libraries"))) + { + QJsonObject libObj = ensureObject(libVal); + // parse the library + auto lib = RawLibrary::fromJsonPlus(libObj, filename); + out->addLibs.append(lib); + } + } + + if (root.contains("-libraries")) + { + for (auto libVal : ensureArray(root.value("-libraries"))) + { + auto libObj = ensureObject(libVal); + out->removeLibs.append(ensureString(libObj.value("name"))); + } + } + return out; +} + +QJsonDocument VersionFile::toJson(bool saveOrder) +{ + QJsonObject root; + if (saveOrder) + { + root.insert("order", order); + } + writeString(root, "name", name); + writeString(root, "fileId", fileId); + writeString(root, "version", version); + writeString(root, "mcVersion", mcVersion); + writeString(root, "id", id); + writeString(root, "mainClass", mainClass); + writeString(root, "appletClass", appletClass); + writeString(root, "processArguments", processArguments); + writeString(root, "minecraftArguments", overwriteMinecraftArguments); + writeString(root, "+minecraftArguments", addMinecraftArguments); + writeString(root, "-minecraftArguments", removeMinecraftArguments); + writeString(root, "type", type); + writeString(root, "assets", assets); + if (isMinecraftVersion()) + { + writeString(root, "releaseTime", m_releaseTimeString); + writeString(root, "time", m_updateTimeString); + } + if (minimumLauncherVersion != -1) + { + root.insert("minimumLauncherVersion", minimumLauncherVersion); + } + writeStringList(root, "tweakers", overwriteTweakers); + writeStringList(root, "+tweakers", addTweakers); + writeStringList(root, "-tweakers", removeTweakers); + writeStringList(root, "+traits", traits.toList()); + writeObjectList(root, "libraries", overwriteLibs); + writeObjectList(root, "+libraries", addLibs); + writeObjectList(root, "+jarMods", jarMods); + // FIXME: removed libs are special snowflakes. + if (removeLibs.size()) + { + QJsonArray array; + for (auto lib : removeLibs) + { + QJsonObject rmlibobj; + rmlibobj.insert("name", lib); + array.append(rmlibobj); + } + root.insert("-libraries", array); + } + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +bool VersionFile::isMinecraftVersion() +{ + return (fileId == "org.multimc.version.json") || (fileId == "net.minecraft") || + (fileId == "org.multimc.custom.json"); +} + +bool VersionFile::hasJarMods() +{ + return !jarMods.isEmpty(); +} + +void VersionFile::applyTo(InstanceVersion *version) +{ + if (minimumLauncherVersion != -1) + { + if (minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) + { + throw LauncherVersionError(minimumLauncherVersion, + CURRENT_MINIMUM_LAUNCHER_VERSION); + } + } + + if (!version->id.isNull() && !mcVersion.isNull()) + { + if (QRegExp(mcVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(version->id) == + -1) + { + throw MinecraftVersionMismatch(fileId, mcVersion, version->id); + } + } + + if (!id.isNull()) + { + version->id = id; + } + if (!mainClass.isNull()) + { + version->mainClass = mainClass; + } + if (!appletClass.isNull()) + { + version->appletClass = appletClass; + } + if (!processArguments.isNull()) + { + if (isMinecraftVersion()) + { + version->vanillaProcessArguments = processArguments; + } + version->processArguments = processArguments; + } + if (isMinecraftVersion()) + { + if (!type.isNull()) + { + version->type = type; + } + if (!m_releaseTimeString.isNull()) + { + version->m_releaseTimeString = m_releaseTimeString; + version->m_releaseTime = m_releaseTime; + } + if (!m_updateTimeString.isNull()) + { + version->m_updateTimeString = m_updateTimeString; + version->m_updateTime = m_updateTime; + } + } + if (!assets.isNull()) + { + version->assets = assets; + } + if (minimumLauncherVersion >= 0) + { + if (version->minimumLauncherVersion < minimumLauncherVersion) + version->minimumLauncherVersion = minimumLauncherVersion; + } + if (!overwriteMinecraftArguments.isNull()) + { + if (isMinecraftVersion()) + { + version->vanillaMinecraftArguments = overwriteMinecraftArguments; + } + version->minecraftArguments = overwriteMinecraftArguments; + } + if (!addMinecraftArguments.isNull()) + { + version->minecraftArguments += addMinecraftArguments; + } + if (!removeMinecraftArguments.isNull()) + { + version->minecraftArguments.remove(removeMinecraftArguments); + } + if (shouldOverwriteTweakers) + { + version->tweakers = overwriteTweakers; + } + for (auto tweaker : addTweakers) + { + version->tweakers += tweaker; + } + for (auto tweaker : removeTweakers) + { + version->tweakers.removeAll(tweaker); + } + version->jarMods.append(jarMods); + version->traits.unite(traits); + if (shouldOverwriteLibs) + { + QList<OneSixLibraryPtr> libs; + for (auto lib : overwriteLibs) + { + libs.append(OneSixLibrary::fromRawLibrary(lib)); + } + if (isMinecraftVersion()) + { + version->vanillaLibraries = libs; + } + version->libraries = libs; + } + for (auto lib : addLibs) + { + switch (lib->insertType) + { + case RawLibrary::Apply: + { + // QLOG_INFO() << "Applying lib " << lib->name; + int index = findLibrary(version->libraries, lib->m_name); + if (index >= 0) + { + auto library = version->libraries[index]; + if (!lib->m_base_url.isNull()) + { + library->setBaseUrl(lib->m_base_url); + } + if (!lib->m_hint.isNull()) + { + library->setHint(lib->m_hint); + } + if (!lib->m_absolute_url.isNull()) + { + library->setAbsoluteUrl(lib->m_absolute_url); + } + if (lib->applyExcludes) + { + library->extract_excludes = lib->extract_excludes; + } + if (lib->isNative()) + { + // library->clearSuffixes(); + library->m_native_suffixes = lib->m_native_suffixes; + /* + for (auto native : lib->natives) + { + library->addNative(native.first, native.second); + } + */ + } + if (lib->applyRules) + { + library->setRules(lib->m_rules); + } + library->finalize(); + } + else + { + QLOG_WARN() << "Couldn't find" << lib->m_name << "(skipping)"; + } + break; + } + case RawLibrary::Append: + case RawLibrary::Prepend: + { + // QLOG_INFO() << "Adding lib " << lib->name; + const int startOfVersion = lib->m_name.lastIndexOf(':') + 1; + const int index = findLibrary( + version->libraries, QString(lib->m_name).replace(startOfVersion, INT_MAX, '*')); + if (index < 0) + { + if (lib->insertType == RawLibrary::Append) + { + version->libraries.append(OneSixLibrary::fromRawLibrary(lib)); + } + else + { + version->libraries.prepend(OneSixLibrary::fromRawLibrary(lib)); + } + } + else + { + auto otherLib = version->libraries.at(index); + const Util::Version ourVersion = lib->m_name.mid(startOfVersion, INT_MAX); + const Util::Version otherVersion = otherLib->version(); + // if the existing version is a hard dependency we can either use it or + // fail, but we can't change it + if (otherLib->dependType == OneSixLibrary::Hard) + { + // we need a higher version, or we're hard to and the versions aren't + // equal + if (ourVersion > otherVersion || + (lib->dependType == RawLibrary::Hard && ourVersion != otherVersion)) + { + throw VersionBuildError( + QObject::tr( + "Error resolving library dependencies between %1 and %2 in %3.") + .arg(otherLib->rawName(), lib->m_name, filename)); + } + else + { + // the library is already existing, so we don't have to do anything + } + } + else if (otherLib->dependType == OneSixLibrary::Soft) + { + // if we are higher it means we should update + if (ourVersion > otherVersion) + { + auto library = OneSixLibrary::fromRawLibrary(lib); + if (Util::Version(otherLib->minVersion) < ourVersion) + { + library->minVersion = ourVersion.toString(); + } + version->libraries.replace(index, library); + } + else + { + // our version is smaller than the existing version, but we require + // it: fail + if (lib->dependType == RawLibrary::Hard) + { + throw VersionBuildError(QObject::tr( + "Error resolving library dependencies between %1 and %2 in %3.") + .arg(otherLib->rawName(), lib->m_name, + filename)); + } + } + } + } + break; + } + case RawLibrary::Replace: + { + QString toReplace; + if (lib->insertData.isEmpty()) + { + const int startOfVersion = lib->m_name.lastIndexOf(':') + 1; + toReplace = QString(lib->m_name).replace(startOfVersion, INT_MAX, '*'); + } + else + toReplace = lib->insertData; + // QLOG_INFO() << "Replacing lib " << toReplace << " with " << lib->name; + int index = findLibrary(version->libraries, toReplace); + if (index >= 0) + { + version->libraries.replace(index, OneSixLibrary::fromRawLibrary(lib)); + } + else + { + QLOG_WARN() << "Couldn't find" << toReplace << "(skipping)"; + } + break; + } + } + } + for (auto lib : removeLibs) + { + int index = findLibrary(version->libraries, lib); + if (index >= 0) + { + // QLOG_INFO() << "Removing lib " << lib; + version->libraries.removeAt(index); + } + else + { + QLOG_WARN() << "Couldn't find" << lib << "(skipping)"; + } + } +} diff --git a/logic/minecraft/VersionFile.h b/logic/minecraft/VersionFile.h new file mode 100644 index 00000000..186f4335 --- /dev/null +++ b/logic/minecraft/VersionFile.h @@ -0,0 +1,103 @@ +#pragma once + +#include <QString> +#include <QStringList> +#include <QDateTime> +#include <memory> +#include "logic/minecraft/OpSys.h" +#include "logic/minecraft/OneSixRule.h" +#include "VersionPatch.h" +#include "MMCError.h" +#include "OneSixLibrary.h" +#include "JarMod.h" + +class InstanceVersion; +struct VersionFile; + +typedef std::shared_ptr<VersionFile> VersionFilePtr; +class VersionFile : public VersionPatch +{ +public: /* methods */ + static VersionFilePtr fromJson(const QJsonDocument &doc, const QString &filename, + const bool requireOrder, const bool isFTB = false); + QJsonDocument toJson(bool saveOrder); + + virtual void applyTo(InstanceVersion *version) 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 getPatchID() override + { + return fileId; + } + virtual QString getPatchName() override + { + return name; + } + virtual QString getPatchVersion() override + { + return version; + } + virtual QString getPatchFilename() override + { + return filename; + } + +public: /* data */ + int order = 0; + QString name; + QString fileId; + QString version; + // TODO use the mcVersion to determine if a version file should be removed on update + QString mcVersion; + QString filename; + // TODO requirements + // QMap<QString, QString> requirements; + QString id; + QString mainClass; + QString appletClass; + QString overwriteMinecraftArguments; + QString addMinecraftArguments; + QString removeMinecraftArguments; + QString processArguments; + QString type; + + /// the time this version was actually released by Mojang, as string and as QDateTime + QString m_releaseTimeString; + QDateTime m_releaseTime; + + /// the time this version was last updated by Mojang, as string and as QDateTime + QString m_updateTimeString; + QDateTime m_updateTime; + + /// asset group used by this ... thing. + QString assets; + int minimumLauncherVersion = -1; + + bool shouldOverwriteTweakers = false; + QStringList overwriteTweakers; + QStringList addTweakers; + QStringList removeTweakers; + + bool shouldOverwriteLibs = false; + QList<RawLibraryPtr> overwriteLibs; + QList<RawLibraryPtr> addLibs; + QList<QString> removeLibs; + + QSet<QString> traits; + + QList<JarmodPtr> jarMods; +}; + + diff --git a/logic/minecraft/VersionPatch.h b/logic/minecraft/VersionPatch.h new file mode 100644 index 00000000..1dd30e79 --- /dev/null +++ b/logic/minecraft/VersionPatch.h @@ -0,0 +1,31 @@ +#pragma once + +#include <memory> +#include <QList> +#include "JarMod.h" + +class InstanceVersion; +class VersionPatch +{ +public: + virtual ~VersionPatch(){}; + virtual void applyTo(InstanceVersion *version) = 0; + + virtual bool isMinecraftVersion() = 0; + virtual bool hasJarMods() = 0; + virtual QList<JarmodPtr> getJarMods() = 0; + + virtual bool isMoveable() + { + return getOrder() >= 0; + } + virtual void setOrder(int order) = 0; + virtual int getOrder() = 0; + + virtual QString getPatchID() = 0; + virtual QString getPatchName() = 0; + virtual QString getPatchVersion() = 0; + virtual QString getPatchFilename() = 0; +}; + +typedef std::shared_ptr<VersionPatch> VersionPatchPtr; diff --git a/logic/minecraft/VersionSource.h b/logic/minecraft/VersionSource.h new file mode 100644 index 00000000..75b2c24b --- /dev/null +++ b/logic/minecraft/VersionSource.h @@ -0,0 +1,9 @@ +#pragma once + +/// where is a version from? +enum VersionSource +{ + Builtin, //!< version loaded from the internal resources. + Local, //!< version loaded from a file in the cache. + Remote, //!< incomplete version on a remote server. +};
\ No newline at end of file |