diff options
Diffstat (limited to 'logic/minecraft')
24 files changed, 4231 insertions, 3 deletions
diff --git a/logic/minecraft/AssetsUtils.cpp b/logic/minecraft/AssetsUtils.cpp new file mode 100644 index 00000000..9f33b1bd --- /dev/null +++ b/logic/minecraft/AssetsUtils.cpp @@ -0,0 +1,217 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QDir> +#include <QDirIterator> +#include <QCryptographicHash> +#include <QJsonParseError> +#include <QJsonDocument> +#include <QJsonObject> +#include <QVariant> +#include <QDebug> + +#include "AssetsUtils.h" +#include <pathutils.h> + +namespace AssetsUtils +{ +int findLegacyAssets() +{ + QDir assets_dir("assets"); + if (!assets_dir.exists()) + return 0; + assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + int base_length = assets_dir.path().length(); + + QList<QString> blacklist = {"indexes", "objects", "virtual"}; + + QDirIterator iterator(assets_dir, QDirIterator::Subdirectories); + int found = 0; + while (iterator.hasNext()) + { + QString currentDir = iterator.next(); + currentDir = currentDir.remove(0, base_length + 1); + + bool ignore = false; + for (QString blacklisted : blacklist) + { + if (currentDir.startsWith(blacklisted)) + ignore = true; + } + + if (!iterator.fileInfo().isDir() && !ignore) + { + found++; + } + } + + return found; +} + +/* + * Returns true on success, with index populated + * index is undefined otherwise + */ +bool loadAssetsIndexJson(QString path, AssetsIndex *index) +{ + /* + { + "objects": { + "icons/icon_16x16.png": { + "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", + "size": 3665 + }, + ... + } + } + } + */ + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to read assets index file" << path; + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << "Failed to parse assets index file:" << parseError.errorString() + << "at offset " << QString::number(parseError.offset); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid assets index JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + QJsonValue isVirtual = root.value("virtual"); + if (!isVirtual.isUndefined()) + { + index->isVirtual = isVirtual.toBool(false); + } + + QJsonValue objects = root.value("objects"); + QVariantMap map = objects.toVariant().toMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + // qDebug() << iter.key(); + + QVariant variant = iter.value(); + QVariantMap nested_objects = variant.toMap(); + + AssetObject object; + + for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); + nested_iter != nested_objects.end(); ++nested_iter) + { + // qDebug() << nested_iter.key() << nested_iter.value().toString(); + QString key = nested_iter.key(); + QVariant value = nested_iter.value(); + + if (key == "hash") + { + object.hash = value.toString(); + } + else if (key == "size") + { + object.size = value.toDouble(); + } + } + + index->objects.insert(iter.key(), object); + } + + return true; +} + +QDir reconstructAssets(QString assetsId) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets"; + return virtualRoot; + } + + qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() + << objectDir.path() << virtualDir.path() << virtualRoot.path(); + + AssetsIndex index; + bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(indexPath, &index); + + if (loadAssetsIndex && index.isVirtual) + { + qDebug() << "Reconstructing virtual assets folder at" << virtualRoot.path(); + + for (QString map : index.objects.keys()) + { + AssetObject asset_object = index.objects.value(map); + QString target_path = PathCombine(virtualRoot.path(), map); + QFile target(target_path); + + QString tlk = asset_object.hash.left(2); + + QString original_path = + PathCombine(PathCombine(objectDir.path(), tlk), asset_object.hash); + QFile original(original_path); + if (!original.exists()) + continue; + if (!target.exists()) + { + QFileInfo info(target_path); + QDir target_dir = info.dir(); + // qDebug() << target_dir; + if (!target_dir.exists()) + QDir("").mkpath(target_dir.path()); + + bool couldCopy = original.copy(target_path); + qDebug() << " Copying" << original_path << "to" << target_path + << QString::number(couldCopy); // << original.errorString(); + } + } + + // TODO: Write last used time to virtualRoot/.lastused + } + + return virtualRoot; +} + +} diff --git a/logic/minecraft/AssetsUtils.h b/logic/minecraft/AssetsUtils.h new file mode 100644 index 00000000..ea12136d --- /dev/null +++ b/logic/minecraft/AssetsUtils.h @@ -0,0 +1,39 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QString> +#include <QMap> + +struct AssetObject +{ + QString hash; + qint64 size; +}; + +struct AssetsIndex +{ + QMap<QString, AssetObject> objects; + bool isVirtual = false; +}; + +namespace AssetsUtils +{ +bool loadAssetsIndexJson(QString file, AssetsIndex* index); +int findLegacyAssets(); +/// Reconstruct a virtual assets folder for the given assets ID and return the folder +QDir reconstructAssets(QString assetsId); +} diff --git a/logic/minecraft/JarUtils.cpp b/logic/minecraft/JarUtils.cpp new file mode 100644 index 00000000..59326aba --- /dev/null +++ b/logic/minecraft/JarUtils.cpp @@ -0,0 +1,158 @@ +#include "logic/minecraft/JarUtils.h" +#include <quazip.h> +#include <quazipfile.h> +#include <JlCompress.h> +#include <QDebug> + +namespace JarUtils { + +bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, + std::function<bool(QString)> filter) +{ + QuaZip modZip(from.filePath()); + modZip.open(QuaZip::mdUnzip); + + QuaZipFile fileInsideMod(&modZip); + QuaZipFile zipOutFile(into); + for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) + { + QString filename = modZip.getCurrentFileName(); + if (!filter(filename)) + { + qDebug() << "Skipping file " << filename << " from " + << from.fileName() << " - filtered"; + continue; + } + if (contained.contains(filename)) + { + qDebug() << "Skipping already contained file " << filename << " from " + << from.fileName(); + continue; + } + contained.insert(filename); + + if (!fileInsideMod.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open " << filename << " from " << from.fileName(); + return false; + } + + QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); + + if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) + { + qCritical() << "Failed to open " << filename << " in the jar"; + fileInsideMod.close(); + return false; + } + if (!JlCompress::copyData(fileInsideMod, zipOutFile)) + { + zipOutFile.close(); + fileInsideMod.close(); + qCritical() << "Failed to copy data of " << filename << " into the jar"; + return false; + } + zipOutFile.close(); + fileInsideMod.close(); + } + return true; +} + +bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods) +{ + QuaZip zipOut(targetJarPath); + if (!zipOut.open(QuaZip::mdCreate)) + { + QFile::remove(targetJarPath); + qCritical() << "Failed to open the minecraft.jar for modding"; + return false; + } + // Files already added to the jar. + // These files will be skipped. + QSet<QString> addedFiles; + + // Modify the jar + QListIterator<Mod> i(mods); + i.toBack(); + while (i.hasPrevious()) + { + const Mod &mod = i.previous(); + // do not merge disabled mods. + if (!mod.enabled()) + continue; + if (mod.type() == Mod::MOD_ZIPFILE) + { + if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles, noFilter)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + } + else if (mod.type() == Mod::MOD_SINGLEFILE) + { + auto filename = mod.filename(); + if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), + filename.fileName())) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + addedFiles.insert(filename.fileName()); + } + else if (mod.type() == Mod::MOD_FOLDER) + { + auto filename = mod.filename(); + QString what_to_zip = filename.absoluteFilePath(); + QDir dir(what_to_zip); + dir.cdUp(); + QString parent_dir = dir.absolutePath(); + if (!JlCompress::compressSubDir(&zipOut, what_to_zip, parent_dir, true, addedFiles)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + qDebug() << "Adding folder " << filename.fileName() << " from " + << filename.absoluteFilePath(); + } + } + + if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, metaInfFilter)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to insert minecraft.jar contents."; + return false; + } + + // Recompress the jar + zipOut.close(); + if (zipOut.getZipError() != 0) + { + QFile::remove(targetJarPath); + qCritical() << "Failed to finalize minecraft.jar!"; + return false; + } + return true; +} + +bool noFilter(QString) +{ + return true; +} + +bool metaInfFilter(QString key) +{ + if(key.contains("META-INF")) + { + return false; + } + return true; +} + +} diff --git a/logic/minecraft/JarUtils.h b/logic/minecraft/JarUtils.h new file mode 100644 index 00000000..2e8bd2a7 --- /dev/null +++ b/logic/minecraft/JarUtils.h @@ -0,0 +1,18 @@ +#pragma once +#include <QString> +#include <QFileInfo> +#include <QSet> +#include "Mod.h" +#include <functional> + +class QuaZip; +namespace JarUtils +{ + bool noFilter(QString); + bool metaInfFilter(QString key); + + bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, + std::function<bool(QString)> filter); + + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods); +} diff --git a/logic/minecraft/LegacyInstance.cpp b/logic/minecraft/LegacyInstance.cpp new file mode 100644 index 00000000..c0fe1513 --- /dev/null +++ b/logic/minecraft/LegacyInstance.cpp @@ -0,0 +1,346 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QFileInfo> +#include <QDir> +#include <QImage> +#include <logic/settings/Setting.h> +#include <pathutils.h> +#include <cmdutils.h> + +#include "LegacyInstance.h" + +#include "logic/minecraft/LegacyUpdate.h" +#include "logic/icons/IconList.h" +#include "logic/minecraft/MinecraftProcess.h" +#include "gui/pages/LegacyUpgradePage.h" +#include "gui/pages/ModFolderPage.h" +#include "gui/pages/LegacyJarModPage.h" +#include <gui/pages/TexturePackPage.h> +#include <gui/pages/InstanceSettingsPage.h> +#include <gui/pages/NotesPage.h> +#include <gui/pages/ScreenshotsPage.h> + +LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : MinecraftInstance(globalSettings, settings, rootDir) +{ + m_lwjglFolderSetting = globalSettings->getSetting("LWJGLDir"); + settings->registerSetting("NeedsRebuild", true); + settings->registerSetting("ShouldUpdate", false); + settings->registerSetting("JarVersion", "Unknown"); + settings->registerSetting("LwjglVersion", "2.9.0"); + settings->registerSetting("IntendedJarVersion", ""); + /* + * custom base jar has no default. it is determined in code... see the accessor methods for + *it + * + * for instances that DO NOT have the CustomBaseJar setting (legacy instances), + * [.]minecraft/bin/mcbackup.jar is the default base jar + */ + settings->registerSetting("UseCustomBaseJar", true); + settings->registerSetting("CustomBaseJar", ""); +} + +QList<BasePage *> LegacyInstance::getPages() +{ + QList<BasePage *> values; + // FIXME: actually implement the legacy instance upgrade, then enable this. + //values.append(new LegacyUpgradePage(this)); + values.append(new LegacyJarModPage(this)); + values.append(new ModFolderPage(this, loaderModList(), "mods", "loadermods", tr("Loader mods"), + "Loader-mods")); + values.append(new ModFolderPage(this, coreModList(), "coremods", "coremods", tr("Core mods"), + "Loader-mods")); + values.append(new TexturePackPage(this)); + values.append(new NotesPage(this)); + values.append(new ScreenshotsPage(PathCombine(minecraftRoot(), "screenshots"))); + values.append(new InstanceSettingsPage(this)); + return values; +} + +QString LegacyInstance::dialogTitle() +{ + return tr("Edit Instance (%1)").arg(name()); +} + +QString LegacyInstance::baseJar() const +{ + bool customJar = m_settings->get("UseCustomBaseJar").toBool(); + if (customJar) + { + return customBaseJar(); + } + else + return defaultBaseJar(); +} + +QString LegacyInstance::customBaseJar() const +{ + QString value = m_settings->get("CustomBaseJar").toString(); + if (value.isNull() || value.isEmpty()) + { + return defaultCustomBaseJar(); + } + return value; +} + +void LegacyInstance::setCustomBaseJar(QString val) +{ + if (val.isNull() || val.isEmpty() || val == defaultCustomBaseJar()) + m_settings->reset("CustomBaseJar"); + else + m_settings->set("CustomBaseJar", val); +} + +void LegacyInstance::setShouldUseCustomBaseJar(bool val) +{ + m_settings->set("UseCustomBaseJar", val); +} + +bool LegacyInstance::shouldUseCustomBaseJar() const +{ + return m_settings->get("UseCustomBaseJar").toBool(); +} + + +std::shared_ptr<Task> LegacyInstance::doUpdate() +{ + // make sure the jar mods list is initialized by asking for it. + auto list = jarModList(); + // create an update task + return std::shared_ptr<Task>(new LegacyUpdate(this, this)); +} + +BaseProcess *LegacyInstance::prepareForLaunch(AuthSessionPtr account) +{ + QString launchScript; + QIcon icon = ENV.icons()->getIcon(iconKey()); + auto pixmap = icon.pixmap(128, 128); + pixmap.save(PathCombine(minecraftRoot(), "icon.png"), "PNG"); + + // create the launch script + { + // window size + QString windowParams; + if (settings().get("LaunchMaximized").toBool()) + windowParams = "max"; + else + windowParams = QString("%1x%2") + .arg(settings().get("MinecraftWinWidth").toInt()) + .arg(settings().get("MinecraftWinHeight").toInt()); + + QString lwjgl = QDir(m_lwjglFolderSetting->get().toString() + "/" + lwjglVersion()) + .absolutePath(); + launchScript += "userName " + account->player_name + "\n"; + launchScript += "sessionId " + account->session + "\n"; + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + launchScript += "lwjgl " + lwjgl + "\n"; + launchScript += "launcher legacy\n"; + } + auto process = MinecraftProcess::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr())); + process->setLaunchScript(launchScript); + process->setWorkdir(minecraftRoot()); + process->setLogin(account); + return process; +} + +void LegacyInstance::cleanupAfterRun() +{ + // FIXME: delete the launcher and icons and whatnot. +} + +std::shared_ptr<ModList> LegacyInstance::coreModList() const +{ + if (!core_mod_list) + { + core_mod_list.reset(new ModList(coreModsDir())); + } + core_mod_list->update(); + return core_mod_list; +} + +std::shared_ptr<ModList> LegacyInstance::jarModList() const +{ + if (!jar_mod_list) + { + auto list = new ModList(jarModsDir(), modListFile()); + connect(list, SIGNAL(changed()), SLOT(jarModsChanged())); + jar_mod_list.reset(list); + } + jar_mod_list->update(); + return jar_mod_list; +} + +QList<Mod> LegacyInstance::getJarMods() const +{ + return jarModList()->allMods(); +} + +void LegacyInstance::jarModsChanged() +{ + qDebug() << "Jar mods of instance " << name() << " have changed. Jar will be rebuilt."; + setShouldRebuild(true); +} + +std::shared_ptr<ModList> LegacyInstance::loaderModList() const +{ + if (!loader_mod_list) + { + loader_mod_list.reset(new ModList(loaderModsDir())); + } + loader_mod_list->update(); + return loader_mod_list; +} + +std::shared_ptr<ModList> LegacyInstance::texturePackList() const +{ + if (!texture_pack_list) + { + texture_pack_list.reset(new ModList(texturePacksDir())); + } + texture_pack_list->update(); + return texture_pack_list; +} + +QString LegacyInstance::jarModsDir() const +{ + return PathCombine(instanceRoot(), "instMods"); +} + +QString LegacyInstance::binDir() const +{ + return PathCombine(minecraftRoot(), "bin"); +} + +QString LegacyInstance::libDir() const +{ + return PathCombine(minecraftRoot(), "lib"); +} + +QString LegacyInstance::savesDir() const +{ + return PathCombine(minecraftRoot(), "saves"); +} + +QString LegacyInstance::loaderModsDir() const +{ + return PathCombine(minecraftRoot(), "mods"); +} + +QString LegacyInstance::coreModsDir() const +{ + return PathCombine(minecraftRoot(), "coremods"); +} + +QString LegacyInstance::resourceDir() const +{ + return PathCombine(minecraftRoot(), "resources"); +} +QString LegacyInstance::texturePacksDir() const +{ + return PathCombine(minecraftRoot(), "texturepacks"); +} + +QString LegacyInstance::runnableJar() const +{ + return PathCombine(binDir(), "minecraft.jar"); +} + +QString LegacyInstance::modListFile() const +{ + return PathCombine(instanceRoot(), "modlist"); +} + +QString LegacyInstance::instanceConfigFolder() const +{ + return PathCombine(minecraftRoot(), "config"); +} + +bool LegacyInstance::shouldRebuild() const +{ + return m_settings->get("NeedsRebuild").toBool(); +} + +void LegacyInstance::setShouldRebuild(bool val) +{ + m_settings->set("NeedsRebuild", val); +} + +QString LegacyInstance::currentVersionId() const +{ + return m_settings->get("JarVersion").toString(); +} + +QString LegacyInstance::lwjglVersion() const +{ + return m_settings->get("LwjglVersion").toString(); +} + +void LegacyInstance::setLWJGLVersion(QString val) +{ + m_settings->set("LwjglVersion", val); +} + +QString LegacyInstance::intendedVersionId() const +{ + return m_settings->get("IntendedJarVersion").toString(); +} + +bool LegacyInstance::setIntendedVersionId(QString version) +{ + settings().set("IntendedJarVersion", version); + setShouldUpdate(true); + return true; +} + +bool LegacyInstance::shouldUpdate() const +{ + QVariant var = settings().get("ShouldUpdate"); + if (!var.isValid() || var.toBool() == false) + { + return intendedVersionId() != currentVersionId(); + } + return true; +} + +void LegacyInstance::setShouldUpdate(bool val) +{ + settings().set("ShouldUpdate", val); +} + +QString LegacyInstance::defaultBaseJar() const +{ + return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar"; +} + +QString LegacyInstance::defaultCustomBaseJar() const +{ + return PathCombine(binDir(), "mcbackup.jar"); +} + +QString LegacyInstance::getStatusbarDescription() +{ + if (flags() & VersionBrokenFlag) + { + return tr("Legacy : %1 (broken)").arg(intendedVersionId()); + } + return tr("Legacy : %1").arg(intendedVersionId()); +} + +QString LegacyInstance::lwjglFolder() const +{ + return m_lwjglFolderSetting->get().toString(); +} diff --git a/logic/minecraft/LegacyInstance.h b/logic/minecraft/LegacyInstance.h new file mode 100644 index 00000000..353718c1 --- /dev/null +++ b/logic/minecraft/LegacyInstance.h @@ -0,0 +1,127 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "logic/minecraft/MinecraftInstance.h" +#include "gui/pages/BasePageProvider.h" + +class ModList; +class Task; + +class LegacyInstance : public MinecraftInstance, public BasePageProvider +{ + Q_OBJECT +public: + + explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + + virtual void init() {}; + + /// Path to the instance's minecraft.jar + QString runnableJar() const; + + //! Path to the instance's modlist file. + QString modListFile() const; + + ////// Edit Instance Dialog stuff ////// + virtual QList<BasePage *> getPages(); + virtual QString dialogTitle(); + + ////// Mod Lists ////// + std::shared_ptr<ModList> jarModList() const ; + virtual QList< Mod > getJarMods() const override; + std::shared_ptr<ModList> coreModList() const; + std::shared_ptr<ModList> loaderModList() const; + std::shared_ptr<ModList> texturePackList() const override; + + ////// Directories ////// + QString libDir() const; + QString savesDir() const; + QString texturePacksDir() const; + QString jarModsDir() const; + QString binDir() const; + QString loaderModsDir() const; + QString coreModsDir() const; + QString resourceDir() const; + virtual QString instanceConfigFolder() const override; + + /// Get the curent base jar of this instance. By default, it's the + /// versions/$version/$version.jar + QString baseJar() const; + + /// the default base jar of this instance + QString defaultBaseJar() const; + /// the default custom base jar of this instance + QString defaultCustomBaseJar() const; + + /*! + * Whether or not custom base jar is used + */ + bool shouldUseCustomBaseJar() const; + void setShouldUseCustomBaseJar(bool val); + + /*! + * The value of the custom base jar + */ + QString customBaseJar() const; + void setCustomBaseJar(QString val); + + /*! + * Whether or not the instance's minecraft.jar needs to be rebuilt. + * If this is true, when the instance launches, its jar mods will be + * re-added to a fresh minecraft.jar file. + */ + bool shouldRebuild() const; + void setShouldRebuild(bool val); + + virtual QString currentVersionId() const override; + + //! The version of LWJGL that this instance uses. + QString lwjglVersion() const; + + //! Where the lwjgl versions foor this instance can be found... HACK HACK HACK + QString lwjglFolder() const; + + /// st the version of LWJGL libs this instance will use + void setLWJGLVersion(QString val); + + virtual QString intendedVersionId() const override; + virtual bool setIntendedVersionId(QString version) override; + + virtual QSet<QString> traits() + { + return {"legacy-instance", "texturepacks"}; + }; + + virtual bool shouldUpdate() const override; + virtual void setShouldUpdate(bool val) override; + virtual std::shared_ptr<Task> doUpdate() override; + + virtual BaseProcess *prepareForLaunch(AuthSessionPtr account) override; + virtual void cleanupAfterRun() override; + + virtual QString getStatusbarDescription() override; + +protected: + mutable std::shared_ptr<ModList> jar_mod_list; + mutable std::shared_ptr<ModList> core_mod_list; + mutable std::shared_ptr<ModList> loader_mod_list; + mutable std::shared_ptr<ModList> texture_pack_list; + std::shared_ptr<Setting> m_lwjglFolderSetting; +protected +slots: + virtual void jarModsChanged(); +}; diff --git a/logic/minecraft/LegacyUpdate.cpp b/logic/minecraft/LegacyUpdate.cpp new file mode 100644 index 00000000..d853536a --- /dev/null +++ b/logic/minecraft/LegacyUpdate.cpp @@ -0,0 +1,467 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QStringList> +#include <pathutils.h> +#include <quazip.h> +#include <quazipfile.h> +#include <JlCompress.h> +#include <QDebug> + +#include "logic/Env.h" +#include "logic/BaseInstance.h" +#include "logic/net/URLConstants.h" + +#include "logic/minecraft/JarUtils.h" +#include "logic/minecraft/LegacyUpdate.h" +#include "logic/minecraft/LwjglVersionList.h" +#include "logic/minecraft/MinecraftVersionList.h" +#include "logic/minecraft/ModList.h" +#include "logic/minecraft/LegacyInstance.h" + +LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void LegacyUpdate::executeTask() +{ + fmllibsStart(); +} + +void LegacyUpdate::fmllibsStart() +{ + // Get the mod list + LegacyInstance *inst = (LegacyInstance *)m_inst; + auto modList = inst->jarModList(); + + bool forge_present = false; + + QString version = inst->intendedVersionId(); + auto & fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + lwjglStart(); + return; + } + + auto &libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + for (unsigned i = 0; i < modList->size(); i++) + { + auto &mod = modList->operator[](i); + + // do not use disabled mods. + if (!mod.enabled()) + continue; + + if (mod.type() != Mod::MOD_ZIPFILE) + continue; + + if (mod.mmc_id().contains("forge", Qt::CaseInsensitive)) + { + forge_present = true; + break; + } + if (mod.mmc_id().contains("fml", Qt::CaseInsensitive)) + { + forge_present = true; + break; + } + } + // we don't... + if (!forge_present) + { + lwjglStart(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto &lib : libList) + { + QFileInfo libInfo(PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + lwjglStart(); + return; + } + + // download missing libs to our place + setStatus(tr("Dowloading FML libraries...")); + auto dljob = new NetJob("FML libraries"); + auto metacache = ENV.metacache(); + for (auto &lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename + : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; + dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry)); + } + + connect(dljob, SIGNAL(succeeded()), SLOT(fmllibsFinished())); + connect(dljob, SIGNAL(failed()), SLOT(fmllibsFailed())); + connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + legacyDownloadJob.reset(dljob); + legacyDownloadJob->start(); +} + +void LegacyUpdate::fmllibsFinished() +{ + legacyDownloadJob.reset(); + if(!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + LegacyInstance *inst = (LegacyInstance *)m_inst; + auto metacache = ENV.metacache(); + int index = 0; + for (auto &lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = PathCombine(inst->libDir(), lib.filename); + if(!ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + lwjglStart(); +} + +void LegacyUpdate::fmllibsFailed() +{ + emitFailed("Game update failed: it was impossible to fetch the required FML libraries."); + return; +} + +void LegacyUpdate::lwjglStart() +{ + LegacyInstance *inst = (LegacyInstance *)m_inst; + + lwjglVersion = inst->lwjglVersion(); + lwjglTargetPath = PathCombine(inst->lwjglFolder(), lwjglVersion); + lwjglNativesPath = PathCombine(lwjglTargetPath, "natives"); + + // if the 'done' file exists, we don't have to download this again + QFileInfo doneFile(PathCombine(lwjglTargetPath, "done")); + if (doneFile.exists()) + { + jarStart(); + return; + } + + auto list = std::dynamic_pointer_cast<LWJGLVersionList>(ENV.getVersionList("org.lwjgl.legacy")); + if (!list->isLoaded()) + { + emitFailed("Too soon! Let the LWJGL list load :)"); + return; + } + + setStatus(tr("Downloading new LWJGL...")); + auto version = std::dynamic_pointer_cast<LWJGLVersion>(list->findVersion(lwjglVersion)); + if (!version) + { + emitFailed("Game update failed: the selected LWJGL version is invalid."); + return; + } + + QString url = version->url(); + QUrl realUrl(url); + QString hostname = realUrl.host(); + auto worker = ENV.qnam(); + QNetworkRequest req(realUrl); + req.setRawHeader("Host", hostname.toLatin1()); + req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); + QNetworkReply *rep = worker->get(req); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + connect(worker.get(), SIGNAL(finished(QNetworkReply *)), + SLOT(lwjglFinished(QNetworkReply *))); +} + +void LegacyUpdate::lwjglFinished(QNetworkReply *reply) +{ + if (m_reply.get() != reply) + { + return; + } + if (reply->error() != QNetworkReply::NoError) + { + emitFailed("Failed to download: " + reply->errorString() + + "\nSometimes you have to wait a bit if you download many LWJGL versions in " + "a row. YMMV"); + return; + } + auto worker = ENV.qnam(); + // Here i check if there is a cookie for me in the reply and extract it + QList<QNetworkCookie> cookies = + qvariant_cast<QList<QNetworkCookie>>(reply->header(QNetworkRequest::SetCookieHeader)); + if (cookies.count() != 0) + { + // you must tell which cookie goes with which url + worker->cookieJar()->setCookiesFromUrl(cookies, QUrl("sourceforge.net")); + } + + // here you can check for the 302 or whatever other header i need + QVariant newLoc = reply->header(QNetworkRequest::LocationHeader); + if (newLoc.isValid()) + { + QString redirectedTo = reply->header(QNetworkRequest::LocationHeader).toString(); + QUrl realUrl(redirectedTo); + QString hostname = realUrl.host(); + QNetworkRequest req(redirectedTo); + req.setRawHeader("Host", hostname.toLatin1()); + req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); + QNetworkReply *rep = worker->get(req); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + m_reply = std::shared_ptr<QNetworkReply>(rep); + return; + } + QFile saveMe("lwjgl.zip"); + saveMe.open(QIODevice::WriteOnly); + saveMe.write(m_reply->readAll()); + saveMe.close(); + setStatus(tr("Installing new LWJGL...")); + extractLwjgl(); + jarStart(); +} +void LegacyUpdate::extractLwjgl() +{ + // make sure the directories are there + + bool success = ensureFolderPathExists(lwjglNativesPath); + + if (!success) + { + emitFailed("Failed to extract the lwjgl libs - error when creating required folders."); + return; + } + + QuaZip zip("lwjgl.zip"); + if (!zip.open(QuaZip::mdUnzip)) + { + emitFailed("Failed to extract the lwjgl libs - not a valid archive."); + return; + } + + // and now we are going to access files inside it + QuaZipFile file(&zip); + const QString jarNames[] = {"jinput.jar", "lwjgl_util.jar", "lwjgl.jar"}; + for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile()) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + emitFailed("Failed to extract the lwjgl libs - error while reading archive."); + return; + } + QuaZipFileInfo info; + QString name = file.getActualFileName(); + if (name.endsWith('/')) + { + file.close(); + continue; + } + QString destFileName; + // Look for the jars + for (int i = 0; i < 3; i++) + { + if (name.endsWith(jarNames[i])) + { + destFileName = PathCombine(lwjglTargetPath, jarNames[i]); + } + } + // Not found? look for the natives + if (destFileName.isEmpty()) + { +#ifdef Q_OS_WIN32 + QString nativesDir = "windows"; +#else +#ifdef Q_OS_MAC + QString nativesDir = "macosx"; +#else + QString nativesDir = "linux"; +#endif +#endif + if (name.contains(nativesDir)) + { + int lastSlash = name.lastIndexOf('/'); + int lastBackSlash = name.lastIndexOf('\\'); + if (lastSlash != -1) + name = name.mid(lastSlash + 1); + else if (lastBackSlash != -1) + name = name.mid(lastBackSlash + 1); + destFileName = PathCombine(lwjglNativesPath, name); + } + } + // Now if destFileName is still empty, go to the next file. + if (!destFileName.isEmpty()) + { + setStatus(tr("Installing new LWJGL - extracting ") + name + "..."); + QFile output(destFileName); + output.open(QIODevice::WriteOnly); + output.write(file.readAll()); // FIXME: wste of memory!? + output.close(); + } + file.close(); // do not forget to close! + } + zip.close(); + m_reply.reset(); + QFile doneFile(PathCombine(lwjglTargetPath, "done")); + doneFile.open(QIODevice::WriteOnly); + doneFile.write("done."); + doneFile.close(); +} + +void LegacyUpdate::lwjglFailed() +{ + emitFailed("Bad stuff happened while trying to get the lwjgl libs..."); +} + +void LegacyUpdate::jarStart() +{ + LegacyInstance *inst = (LegacyInstance *)m_inst; + if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar()) + { + ModTheJar(); + return; + } + + setStatus(tr("Checking for jar updates...")); + // Make directories + QDir binDir(inst->binDir()); + if (!binDir.exists() && !binDir.mkpath(".")) + { + emitFailed("Failed to create bin folder."); + return; + } + + // Build a list of URLs that will need to be downloaded. + setStatus(tr("Downloading new minecraft.jar ...")); + + QString version_id = inst->intendedVersionId(); + QString localPath = version_id + "/" + version_id + ".jar"; + QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath; + + auto dljob = new NetJob("Minecraft.jar for version " + version_id); + + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + dljob->addNetAction(CacheDownload::make(QUrl(urlstr), entry)); + connect(dljob, SIGNAL(succeeded()), SLOT(jarFinished())); + connect(dljob, SIGNAL(failed()), SLOT(jarFailed())); + connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + legacyDownloadJob.reset(dljob); + legacyDownloadJob->start(); +} + +void LegacyUpdate::jarFinished() +{ + // process the jar + ModTheJar(); +} + +void LegacyUpdate::jarFailed() +{ + // bad, bad + emitFailed("Failed to download the minecraft jar. Try again later."); +} + +void LegacyUpdate::ModTheJar() +{ + LegacyInstance *inst = (LegacyInstance *)m_inst; + + if (!inst->shouldRebuild()) + { + emitSucceeded(); + return; + } + + // Get the mod list + auto modList = inst->getJarMods(); + + QFileInfo runnableJar(inst->runnableJar()); + QFileInfo baseJar(inst->baseJar()); + bool base_is_custom = inst->shouldUseCustomBaseJar(); + + // Nothing to do if there are no jar mods to install, no backup and just the mc jar + if (base_is_custom) + { + // yes, this can happen if the instance only has the runnable jar and not the base jar + // it *could* be assumed that such an instance is vanilla, but that wouldn't be safe + // because that's not something mmc4 guarantees + if (runnableJar.isFile() && !baseJar.exists() && modList.empty()) + { + inst->setShouldRebuild(false); + emitSucceeded(); + return; + } + + setStatus(tr("Installing mods: Backing up minecraft.jar ...")); + if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath())) + { + emitFailed("It seems both the active and base jar are gone. A fresh base jar will " + "be used on next run."); + inst->setShouldRebuild(true); + inst->setShouldUpdate(true); + inst->setShouldUseCustomBaseJar(false); + return; + } + } + + if (!baseJar.exists()) + { + emitFailed("The base jar " + baseJar.filePath() + " does not exist"); + return; + } + + if (runnableJar.exists() && !QFile::remove(runnableJar.filePath())) + { + emitFailed("Failed to delete old minecraft.jar"); + return; + } + + setStatus(tr("Installing mods: Opening minecraft.jar ...")); + + QString outputJarPath = runnableJar.filePath(); + QString inputJarPath = baseJar.filePath(); + + if(!JarUtils::createModdedJar(inputJarPath, outputJarPath, modList)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + inst->setShouldRebuild(false); + // inst->UpdateVersion(true); + emitSucceeded(); + return; +} diff --git a/logic/minecraft/LegacyUpdate.h b/logic/minecraft/LegacyUpdate.h new file mode 100644 index 00000000..78e456a1 --- /dev/null +++ b/logic/minecraft/LegacyUpdate.h @@ -0,0 +1,72 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QObject> +#include <QList> +#include <QUrl> + +#include "logic/net/NetJob.h" +#include "logic/tasks/Task.h" +#include "logic/minecraft/VersionFilterData.h" + +class MinecraftVersion; +class BaseInstance; +class QuaZip; +class Mod; + +class LegacyUpdate : public Task +{ + Q_OBJECT +public: + explicit LegacyUpdate(BaseInstance *inst, QObject *parent = 0); + virtual void executeTask(); + +private +slots: + void lwjglStart(); + void lwjglFinished(QNetworkReply *); + void lwjglFailed(); + + void jarStart(); + void jarFinished(); + void jarFailed(); + + void fmllibsStart(); + void fmllibsFinished(); + void fmllibsFailed(); + + void extractLwjgl(); + + void ModTheJar(); + +private: + + std::shared_ptr<QNetworkReply> m_reply; + + // target version, determined during this task + // MinecraftVersion *targetVersion; + QString lwjglURL; + QString lwjglVersion; + + QString lwjglTargetPath; + QString lwjglNativesPath; + +private: + NetJobPtr legacyDownloadJob; + BaseInstance *m_inst = nullptr; + QList<FMLlib> fmlLibsToProcess; +}; diff --git a/logic/minecraft/LwjglVersionList.cpp b/logic/minecraft/LwjglVersionList.cpp new file mode 100644 index 00000000..9e101b74 --- /dev/null +++ b/logic/minecraft/LwjglVersionList.cpp @@ -0,0 +1,189 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LwjglVersionList.h" +#include "logic/Env.h" + +#include <QtNetwork> +#include <QtXml> +#include <QRegExp> + +#include <QDebug> + +#define RSS_URL "http://sourceforge.net/projects/java-game-lib/rss" + +LWJGLVersionList::LWJGLVersionList(QObject *parent) : BaseVersionList(parent) +{ + setLoading(false); +} + +QVariant LWJGLVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + const PtrLWJGLVersion version = m_vlist.at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + return version->name(); + + case Qt::ToolTipRole: + return version->url(); + + default: + return QVariant(); + } +} + +QVariant LWJGLVersionList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + return tr("Version"); + + case Qt::ToolTipRole: + return tr("LWJGL version name."); + + default: + return QVariant(); + } +} + +int LWJGLVersionList::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +bool LWJGLVersionList::isLoading() const +{ + return m_loading; +} + +void LWJGLVersionList::loadList() +{ + Q_ASSERT_X(!m_loading, "loadList", "list is already loading (m_loading is true)"); + + setLoading(true); + auto worker = ENV.qnam(); + QNetworkRequest req(QUrl(RSS_URL)); + req.setRawHeader("Accept", "application/rss+xml, text/xml, */*"); + req.setRawHeader("User-Agent", "MultiMC/5.0 (Uncached)"); + reply = worker->get(req); + connect(reply, SIGNAL(finished()), SLOT(netRequestComplete())); +} + +inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname) +{ + QDomNodeList elementList = parent.elementsByTagName(tagname); + if (elementList.count()) + return elementList.at(0).toElement(); + else + return QDomElement(); +} + +void LWJGLVersionList::netRequestComplete() +{ + if (reply->error() == QNetworkReply::NoError) + { + QRegExp lwjglRegex("lwjgl-(([0-9]\\.?)+)\\.zip"); + Q_ASSERT_X(lwjglRegex.isValid(), "load LWJGL list", "LWJGL regex is invalid"); + + QDomDocument doc; + + QString xmlErrorMsg; + int errorLine; + auto rawData = reply->readAll(); + if (!doc.setContent(rawData, false, &xmlErrorMsg, &errorLine)) + { + failed("Failed to load LWJGL list. XML error: " + xmlErrorMsg + " at line " + + QString::number(errorLine)); + setLoading(false); + return; + } + + QDomNodeList items = doc.elementsByTagName("item"); + + QList<PtrLWJGLVersion> tempList; + + for (int i = 0; i < items.length(); i++) + { + Q_ASSERT_X(items.at(i).isElement(), "load LWJGL list", + "XML element isn't an element... wat?"); + + QDomElement linkElement = getDomElementByTagName(items.at(i).toElement(), "link"); + if (linkElement.isNull()) + { + qDebug() << "Link element" << i << "in RSS feed doesn't exist! Skipping."; + continue; + } + + QString link = linkElement.text(); + + // Make sure it's a download link. + if (link.endsWith("/download") && link.contains(lwjglRegex)) + { + QString name = link.mid(lwjglRegex.indexIn(link) + 6); + // Subtract 4 here to remove the .zip file extension. + name = name.left(lwjglRegex.matchedLength() - 10); + + QUrl url(link); + if (!url.isValid()) + { + qWarning() << "LWJGL version URL isn't valid:" << link << "Skipping."; + continue; + } + qDebug() << "Discovered LWGL version" << name << "at" << link; + tempList.append(std::make_shared<LWJGLVersion>(name, link)); + } + } + + beginResetModel(); + m_vlist.swap(tempList); + endResetModel(); + + qDebug() << "Loaded LWJGL list."; + finished(); + } + else + { + failed("Failed to load LWJGL list. Network error: " + reply->errorString()); + } + + setLoading(false); + reply->deleteLater(); +} + +void LWJGLVersionList::failed(QString msg) +{ + qCritical() << msg; + emit loadListFailed(msg); +} + +void LWJGLVersionList::finished() +{ + emit loadListFinished(); +} + +void LWJGLVersionList::setLoading(bool loading) +{ + m_loading = loading; + emit loadingStateUpdated(m_loading); +} diff --git a/logic/minecraft/LwjglVersionList.h b/logic/minecraft/LwjglVersionList.h new file mode 100644 index 00000000..c364fa13 --- /dev/null +++ b/logic/minecraft/LwjglVersionList.h @@ -0,0 +1,154 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QObject> +#include <QAbstractListModel> +#include <QUrl> +#include <QNetworkReply> +#include <memory> + +#include "logic/BaseVersion.h" +#include "logic/BaseVersionList.h" + +class LWJGLVersion; +typedef std::shared_ptr<LWJGLVersion> PtrLWJGLVersion; + +class LWJGLVersion : public BaseVersion +{ +public: + LWJGLVersion(const QString &name, const QString &url) + : m_name(name), m_url(url) + { + } + + virtual QString descriptor() + { + return m_name; + } + + virtual QString name() + { + return m_name; + } + + virtual QString typeString() const + { + return QObject::tr("Upstream"); + } + + QString url() const + { + return m_url; + } + +protected: + QString m_name; + QString m_url; +}; + +class LWJGLVersionList : public BaseVersionList +{ + Q_OBJECT +public: + explicit LWJGLVersionList(QObject *parent = 0); + + bool isLoaded() + { + return m_vlist.length() > 0; + } + virtual const BaseVersionPtr at(int i) const override + { + return m_vlist[i]; + } + + virtual Task* getLoadTask() + { + return nullptr; + } + + virtual void sort() {}; + + virtual void updateListData(QList< BaseVersionPtr > versions) {}; + + int count() const + { + return m_vlist.length(); + } + + virtual QVariant data(const QModelIndex &index, int role) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + virtual int rowCount(const QModelIndex &parent) const + { + return count(); + } + virtual int columnCount(const QModelIndex &parent) const; + + virtual bool isLoading() const; + virtual bool errored() const + { + return m_errored; + } + + virtual QString lastErrorMsg() const + { + return m_lastErrorMsg; + } + +public +slots: + /*! + * Loads the version list. + * This is done asynchronously. On success, the loadListFinished() signal will + * be emitted. The list model will be reset as well, resulting in the modelReset() + * signal being emitted. Note that the model will be reset before loadListFinished() is + * emitted. + * If loading the list failed, the loadListFailed(QString msg), + * signal will be emitted. + */ + virtual void loadList(); + +signals: + /*! + * Emitted when the list either starts or finishes loading. + * \param loading Whether or not the list is loading. + */ + void loadingStateUpdated(bool loading); + + void loadListFinished(); + + void loadListFailed(QString msg); + +private: + QList<PtrLWJGLVersion> m_vlist; + + QNetworkReply *m_netReply; + QNetworkReply *reply; + + bool m_loading; + bool m_errored; + QString m_lastErrorMsg; + + void failed(QString msg); + + void finished(); + + void setLoading(bool loading); + +private +slots: + virtual void netRequestComplete(); +}; diff --git a/logic/minecraft/MinecraftInstance.h b/logic/minecraft/MinecraftInstance.h index a6c786fb..63519ae8 100644 --- a/logic/minecraft/MinecraftInstance.h +++ b/logic/minecraft/MinecraftInstance.h @@ -1,5 +1,8 @@ #pragma once #include "logic/BaseInstance.h" +#include "logic/minecraft/Mod.h" + +class ModList; class MinecraftInstance: public BaseInstance { diff --git a/logic/minecraft/MinecraftProfile.cpp b/logic/minecraft/MinecraftProfile.cpp index 9b97b95f..27277ab2 100644 --- a/logic/minecraft/MinecraftProfile.cpp +++ b/logic/minecraft/MinecraftProfile.cpp @@ -23,7 +23,6 @@ #include "logic/minecraft/VersionBuilder.h" #include "ProfileUtils.h" #include "NullProfileStrategy.h" -#include "logic/OneSixInstance.h" MinecraftProfile::MinecraftProfile(ProfileStrategy *strategy) : QAbstractListModel() diff --git a/logic/minecraft/Mod.cpp b/logic/minecraft/Mod.cpp new file mode 100644 index 00000000..7fa4905e --- /dev/null +++ b/logic/minecraft/Mod.cpp @@ -0,0 +1,377 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QDir> +#include <QString> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <quazip.h> +#include <quazipfile.h> + +#include "Mod.h" +#include <pathutils.h> +#include "logic/settings/INIFile.h" +#include <QDebug> + +Mod::Mod(const QFileInfo &file) +{ + repath(file); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + if (m_file.isDir()) + { + m_type = MOD_FOLDER; + m_name = name_base; + m_mmc_id = name_base; + } + else if (m_file.isFile()) + { + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } + else + { + m_enabled = true; + } + m_mmc_id = name_base; + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } + + if (m_type == MOD_ZIPFILE) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); + } + else if (m_type == MOD_FOLDER) + { + QFileInfo mcmod_info(PathCombine(m_file.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + ReadMCModInfo(data); + } + } + else if (m_type == MOD_LITEMOD) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); + } +} + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +void Mod::ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->void + { + if (!arr.at(0).isObject()) + return; + auto firstObj = arr.at(0).toObject(); + m_mod_id = firstObj.value("modid").toString(); + m_name = firstObj.value("name").toString(); + m_version = firstObj.value("version").toString(); + m_homeurl = firstObj.value("url").toString(); + m_updateurl = firstObj.value("updateUrl").toString(); + m_homeurl = m_homeurl.trimmed(); + if(!m_homeurl.isEmpty()) + { + // fix up url. + if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") && + !m_homeurl.startsWith("ftp://")) + { + m_homeurl.prepend("http://"); + } + } + m_description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) + authors = firstObj.value("authors").toArray(); + + if (authors.size() == 0) + m_authors = ""; + else if (authors.size() >= 1) + { + m_authors = authors.at(0).toString(); + for (int i = 1; i < authors.size(); i++) + { + m_authors += ", " + authors.at(i).toString(); + } + } + m_credits = firstObj.value("credits").toString(); + return; + } + ; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) + val = jsonDoc.object().value("modListVersion"); + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) + arrVal = jsonDoc.object().value("modList"); + if (arrVal.isArray()) + { + getInfoFromArray(arrVal.toArray()); + } + } +} + +void Mod::ReadForgeInfo(QByteArray contents) +{ + // Read the data + m_name = "Minecraft Forge"; + m_mod_id = "Forge"; + m_homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + m_version = major + "." + minor + "." + revision + "." + build; +} + +void Mod::ReadLiteModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + m_mod_id = m_name = object.value("name").toString(); + } + if (object.contains("version")) + { + m_version = object.value("version").toString(""); + } + else + { + m_version = object.value("revision").toString(""); + } + m_mcversion = object.value("mcversion").toString(); + m_authors = object.value("author").toString(); + m_description = object.value("description").toString(); + m_homeurl = object.value("url").toString(); +} + +bool Mod::replace(Mod &with) +{ + if (!destroy()) + return false; + bool success = false; + auto t = with.type(); + + if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD) + { + qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath(); + success = QFile::copy(with.m_file.filePath(), m_file.filePath()); + } + if (t == MOD_FOLDER) + { + success = copyPath(with.m_file.filePath(), m_file.path()); + } + if (success) + { + m_name = with.m_name; + m_mmc_id = with.m_mmc_id; + m_mod_id = with.m_mod_id; + m_version = with.m_version; + m_mcversion = with.m_mcversion; + m_description = with.m_description; + m_authors = with.m_authors; + m_credits = with.m_credits; + m_homeurl = with.m_homeurl; + m_type = with.m_type; + m_file.refresh(); + } + return success; +} + +bool Mod::destroy() +{ + if (m_type == MOD_FOLDER) + { + QDir d(m_file.filePath()); + if (d.removeRecursively()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) + { + QFile f(m_file.filePath()); + if (f.remove()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + return true; +} + +QString Mod::version() const +{ + switch (type()) + { + case MOD_ZIPFILE: + case MOD_LITEMOD: + return m_version; + case MOD_FOLDER: + return "Folder"; + case MOD_SINGLEFILE: + return "File"; + default: + return "VOID"; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + m_file = QFileInfo(path); + m_enabled = value; + return true; +} +bool Mod::operator==(const Mod &other) const +{ + return mmc_id() == other.mmc_id(); +} +bool Mod::strongCompare(const Mod &other) const +{ + return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type(); +} diff --git a/logic/minecraft/Mod.h b/logic/minecraft/Mod.h new file mode 100644 index 00000000..5b815bc1 --- /dev/null +++ b/logic/minecraft/Mod.h @@ -0,0 +1,130 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include <QFileInfo> + +class Mod +{ +public: + enum ModType + { + MOD_UNKNOWN, //!< Indicates an unspecified mod type. + MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. + MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). + MOD_FOLDER, //!< The mod is in a folder on the filesystem. + MOD_LITEMOD, //!< The mod is a litemod + }; + + Mod(const QFileInfo &file); + + QFileInfo filename() const + { + return m_file; + } + QString mmc_id() const + { + return m_mmc_id; + } + QString mod_id() const + { + return m_mod_id; + } + ModType type() const + { + return m_type; + } + QString mcversion() const + { + return m_mcversion; + } + ; + bool valid() + { + return m_type != MOD_UNKNOWN; + } + QString name() const + { + return m_name; + } + + QString version() const; + + QString homeurl() const + { + return m_homeurl; + } + + QString description() const + { + return m_description; + } + + QString authors() const + { + return m_authors; + } + + QString credits() const + { + return m_credits; + } + + bool enabled() const + { + return m_enabled; + } + + bool enable(bool value); + + // delete all the files of this mod + bool destroy(); + // replace this mod with a copy of the other + bool replace(Mod &with); + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + // WEAK compare operator - used for replacing mods + bool operator==(const Mod &other) const; + bool strongCompare(const Mod &other) const; + +private: + void ReadMCModInfo(QByteArray contents); + void ReadForgeInfo(QByteArray contents); + void ReadLiteModInfo(QByteArray contents); + +protected: + + // FIXME: what do do with those? HMM... + /* + void ReadModInfoData(QString info); + void ReadForgeInfoData(QString infoFileData); + */ + + QFileInfo m_file; + QString m_mmc_id; + QString m_mod_id; + bool m_enabled = true; + QString m_name; + QString m_version; + QString m_mcversion; + QString m_homeurl; + QString m_updateurl; + QString m_description; + QString m_authors; + QString m_credits; + + ModType m_type; +}; diff --git a/logic/minecraft/ModList.cpp b/logic/minecraft/ModList.cpp new file mode 100644 index 00000000..c943c254 --- /dev/null +++ b/logic/minecraft/ModList.cpp @@ -0,0 +1,610 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModList.h" +#include <pathutils.h> +#include <QMimeData> +#include <QUrl> +#include <QUuid> +#include <QString> +#include <QFileSystemWatcher> +#include <QDebug> + +ModList::ModList(const QString &dir, const QString &list_file) + : QAbstractListModel(), m_dir(dir), m_list_file(list_file) +{ + ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_list_id = QUuid::createUuid().toString(); + m_watcher = new QFileSystemWatcher(this); + is_watching = false; + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void ModList::startWatching() +{ + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void ModList::stopWatching() +{ + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +void ModList::internalSort(QList<Mod> &what) +{ + auto predicate = [](const Mod &left, const Mod &right) + { + if (left.name() == right.name()) + { + return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0; + } + return left.name().localeAwareCompare(right.name()) < 0; + }; + std::sort(what.begin(), what.end(), predicate); +} + +bool ModList::update() +{ + if (!isValid()) + return false; + + QList<Mod> orderedMods; + QList<Mod> newMods; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + bool orderOrStateChanged = false; + + // first, process the ordered items (if any) + OrderList listOrder = readListFile(); + for (auto item : listOrder) + { + QFileInfo infoEnabled(m_dir.filePath(item.id)); + QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled")); + int idxEnabled = folderContents.indexOf(infoEnabled); + int idxDisabled = folderContents.indexOf(infoDisabled); + bool isEnabled; + // if both enabled and disabled versions are present, it's a special case... + if (idxEnabled >= 0 && idxDisabled >= 0) + { + // we only process the one we actually have in the order file. + // and exactly as we have it. + // THIS IS A CORNER CASE + isEnabled = item.enabled; + } + else + { + // only one is present. + // we pick the one that we found. + // we assume the mod was enabled/disabled by external means + isEnabled = idxEnabled >= 0; + } + int idx = isEnabled ? idxEnabled : idxDisabled; + QFileInfo &info = isEnabled ? infoEnabled : infoDisabled; + // if the file from the index file exists + if (idx != -1) + { + // remove from the actual folder contents list + folderContents.takeAt(idx); + // append the new mod + orderedMods.append(Mod(info)); + if (isEnabled != item.enabled) + orderOrStateChanged = true; + } + else + { + orderOrStateChanged = true; + } + } + // if there are any untracked files... + if (folderContents.size()) + { + // the order surely changed! + for (auto entry : folderContents) + { + newMods.append(Mod(entry)); + } + internalSort(newMods); + orderedMods.append(newMods); + orderOrStateChanged = true; + } + // otherwise, if we were already tracking some mods + else if (mods.size()) + { + // if the number doesn't match, order changed. + if (mods.size() != orderedMods.size()) + orderOrStateChanged = true; + // if it does match, compare the mods themselves + else + for (int i = 0; i < mods.size(); i++) + { + if (!mods[i].strongCompare(orderedMods[i])) + { + orderOrStateChanged = true; + break; + } + } + } + beginResetModel(); + mods.swap(orderedMods); + endResetModel(); + if (orderOrStateChanged && !m_list_file.isEmpty()) + { + qDebug() << "Mod list " << m_list_file << " changed!"; + saveListFile(); + emit changed(); + } + return true; +} + +void ModList::directoryChanged(QString path) +{ + update(); +} + +ModList::OrderList ModList::readListFile() +{ + OrderList itemList; + if (m_list_file.isNull() || m_list_file.isEmpty()) + return itemList; + + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return OrderList(); + + QTextStream textStream; + textStream.setAutoDetectUnicode(true); + textStream.setDevice(&textFile); + while (true) + { + QString line = textStream.readLine(); + if (line.isNull() || line.isEmpty()) + break; + else + { + OrderItem it; + it.enabled = !line.endsWith(".disabled"); + if (!it.enabled) + { + line.chop(9); + } + it.id = line; + itemList.append(it); + } + } + textFile.close(); + return itemList; +} + +bool ModList::saveListFile() +{ + if (m_list_file.isNull() || m_list_file.isEmpty()) + return false; + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + return false; + QTextStream textStream; + textStream.setGenerateByteOrderMark(true); + textStream.setCodec("UTF-8"); + textStream.setDevice(&textFile); + for (auto mod : mods) + { + textStream << mod.mmc_id(); + if (!mod.enabled()) + textStream << ".disabled"; + textStream << endl; + } + textFile.close(); + return false; +} + +bool ModList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool ModList::installMod(const QFileInfo &filename, int index) +{ + if (!filename.exists() || !filename.isReadable() || index < 0) + { + return false; + } + Mod m(filename); + if (!m.valid()) + return false; + + // if it's already there, replace the original mod (in place) + int idx = mods.indexOf(m); + if (idx != -1) + { + int idx2 = mods.indexOf(m, idx + 1); + if (idx2 != -1) + return false; + if (mods[idx].replace(m)) + { + + auto left = this->index(index); + auto right = this->index(index, columnCount(QModelIndex()) - 1); + emit dataChanged(left, right); + saveListFile(); + update(); + return true; + } + return false; + } + + auto type = m.type(); + if (type == Mod::MOD_UNKNOWN) + return false; + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + QString newpath = PathCombine(m_dir.path(), filename.fileName()); + if (!QFile::copy(filename.filePath(), newpath)) + return false; + m.repath(newpath); + beginInsertRows(QModelIndex(), index, index); + mods.insert(index, m); + endInsertRows(); + saveListFile(); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + + QString from = filename.filePath(); + QString to = PathCombine(m_dir.path(), filename.fileName()); + if (!copyPath(from, to)) + return false; + m.repath(to); + beginInsertRows(QModelIndex(), index, index); + mods.insert(index, m); + endInsertRows(); + saveListFile(); + update(); + return true; + } + return false; +} + +bool ModList::deleteMod(int index) +{ + if (index >= mods.size() || index < 0) + return false; + Mod &m = mods[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + mods.removeAt(index); + endRemoveRows(); + saveListFile(); + emit changed(); + return true; + } + return false; +} + +bool ModList::deleteMods(int first, int last) +{ + for (int i = first; i <= last; i++) + { + Mod &m = mods[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + mods.erase(mods.begin() + first, mods.begin() + last + 1); + endRemoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModTo(int from, int to) +{ + if (from < 0 || from >= mods.size()) + return false; + if (to >= rowCount()) + to = rowCount() - 1; + if (to == -1) + to = rowCount() - 1; + if (from == to) + return false; + int togap = to > from ? to + 1 : to; + beginMoveRows(QModelIndex(), from, from, QModelIndex(), togap); + mods.move(from, to); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModUp(int from) +{ + if (from > 0) + return moveModTo(from, from - 1); + return false; +} + +bool ModList::moveModsUp(int first, int last) +{ + if (first == 0) + return false; + + beginMoveRows(QModelIndex(), first, last, QModelIndex(), first - 1); + mods.move(first - 1, last); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModDown(int from) +{ + if (from < 0) + return false; + if (from < mods.size() - 1) + return moveModTo(from, from + 1); + return false; +} + +bool ModList::moveModsDown(int first, int last) +{ + if (last == mods.size() - 1) + return false; + + beginMoveRows(QModelIndex(), first, last, QModelIndex(), last + 2); + mods.move(last + 1, first); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +int ModList::columnCount(const QModelIndex &parent) const +{ + return 3; +} + +QVariant ModList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= mods.size()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return mods[row].name(); + case VersionColumn: + return mods[row].version(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto &mod = mods[index.row()]; + if (mod.enable(!mod.enabled())) + { + emit dataChanged(index, index); + return true; + } + } + return false; +} + +QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +QStringList ModList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + types << "text/plain"; + return types; +} + +Qt::DropActions ModList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::DropActions ModList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +QMimeData *ModList::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = new QMimeData(); + + if (indexes.size() == 0) + return data; + + auto idx = indexes[0]; + int row = idx.row(); + if (row < 0 || row >= mods.size()) + return data; + + QStringList params; + params << m_list_id << QString::number(row); + data->setText(params.join('|')); + return data; +} +bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + if (parent.isValid()) + { + row = parent.row(); + column = parent.column(); + } + + if (row > rowCount()) + row = rowCount(); + if (row == -1) + row = rowCount(); + if (column == -1) + column = 0; + qDebug() << "Drop row: " << row << " column: " << column; + + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = is_watching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + installMod(filename, row); + qDebug() << "installing: " << filename; + // if there is no ordering, re-sort the list + if (m_list_file.isEmpty()) + { + beginResetModel(); + internalSort(mods); + endResetModel(); + } + } + if (was_watching) + startWatching(); + return true; + } + else if (data->hasText()) + { + QString sourcestr = data->text(); + auto list = sourcestr.split('|'); + if (list.size() != 2) + return false; + QString remoteId = list[0]; + int remoteIndex = list[1].toInt(); + qDebug() << "move: " << sourcestr; + // no moving of things between two lists + if (remoteId != m_list_id) + return false; + // no point moving to the same place... + if (row == remoteIndex) + return false; + // otherwise, move the mod :D + moveModTo(remoteIndex, row); + return true; + } + return false; +} diff --git a/logic/minecraft/ModList.h b/logic/minecraft/ModList.h new file mode 100644 index 00000000..e227356b --- /dev/null +++ b/logic/minecraft/ModList.h @@ -0,0 +1,158 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QList> +#include <QString> +#include <QDir> +#include <QAbstractListModel> + +#include "logic/minecraft/Mod.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class ModList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + VersionColumn + }; + ModList(const QString &dir, const QString &list_file = QString()); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return size(); + } + ; + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex &parent) const; + + size_t size() const + { + return mods.size(); + } + ; + bool empty() const + { + return size() == 0; + } + Mod &operator[](size_t index) + { + return mods[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + virtual bool installMod(const QFileInfo &filename, int index = 0); + + /// Deletes the mod at the given index. + virtual bool deleteMod(int index); + + /// Deletes all the selected mods + virtual bool deleteMods(int first, int last); + + /** + * move the mod at index to the position N + * 0 is the beginning of the list, length() is the end of the list. + */ + virtual bool moveModTo(int from, int to); + + /** + * move the mod at index one position upwards + */ + virtual bool moveModUp(int from); + virtual bool moveModsUp(int first, int last); + + /** + * move the mod at index one position downwards + */ + virtual bool moveModDown(int from); + virtual bool moveModsDown(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + /// get data for drag action + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() + { + return m_dir; + } + + const QList<Mod> & allMods() + { + return mods; + } + +private: + void internalSort(QList<Mod> & what); + struct OrderItem + { + QString id; + bool enabled = false; + }; + typedef QList<OrderItem> OrderList; + OrderList readListFile(); + bool saveListFile(); +private +slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching; + QDir m_dir; + QString m_list_file; + QString m_list_id; + QList<Mod> mods; +}; diff --git a/logic/minecraft/OneSixInstance.cpp b/logic/minecraft/OneSixInstance.cpp new file mode 100644 index 00000000..ebba22d6 --- /dev/null +++ b/logic/minecraft/OneSixInstance.cpp @@ -0,0 +1,472 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QIcon> +#include <pathutils.h> +#include <QDebug> +#include "MMCError.h" + +#include "logic/minecraft/OneSixInstance.h" + +#include "logic/minecraft/OneSixUpdate.h" +#include "logic/minecraft/MinecraftProfile.h" +#include "logic/minecraft/VersionBuildError.h" +#include "logic/minecraft/MinecraftProcess.h" +#include "logic/minecraft/OneSixProfileStrategy.h" + +#include "logic/minecraft/AssetsUtils.h" +#include "logic/icons/IconList.h" +#include "gui/pagedialog/PageDialog.h" +#include "gui/pages/VersionPage.h" +#include "gui/pages/ModFolderPage.h" +#include "gui/pages/ResourcePackPage.h" +#include "gui/pages/TexturePackPage.h" +#include "gui/pages/InstanceSettingsPage.h" +#include "gui/pages/NotesPage.h" +#include "gui/pages/ScreenshotsPage.h" +#include "gui/pages/OtherLogsPage.h" +OneSixInstance::OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : MinecraftInstance(globalSettings, settings, rootDir) +{ + m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); +} + +void OneSixInstance::init() +{ + createProfile(); +} + +void OneSixInstance::createProfile() +{ + m_version.reset(new MinecraftProfile(new OneSixProfileStrategy(this))); +} + + +QList<BasePage *> OneSixInstance::getPages() +{ + QList<BasePage *> values; + values.append(new VersionPage(this)); + values.append(new ModFolderPage(this, loaderModList(), "mods", "loadermods", + tr("Loader mods"), "Loader-mods")); + values.append(new CoreModFolderPage(this, coreModList(), "coremods", "coremods", + tr("Core mods"), "Core-mods")); + values.append(new ResourcePackPage(this)); + values.append(new TexturePackPage(this)); + values.append(new NotesPage(this)); + values.append(new ScreenshotsPage(PathCombine(minecraftRoot(), "screenshots"))); + values.append(new InstanceSettingsPage(this)); + values.append(new OtherLogsPage(minecraftRoot())); + return values; +} + +QString OneSixInstance::dialogTitle() +{ + return tr("Edit Instance (%1)").arg(name()); +} + +QSet<QString> OneSixInstance::traits() +{ + auto version = getMinecraftProfile(); + if (!version) + { + return {"version-incomplete"}; + } + else + return version->traits; +} + +std::shared_ptr<Task> OneSixInstance::doUpdate() +{ + return std::shared_ptr<Task>(new OneSixUpdate(this)); +} + +QString replaceTokensIn(QString text, QMap<QString, QString> with) +{ + QString result; + QRegExp token_regexp("\\$\\{(.+)\\}"); + token_regexp.setMinimal(true); + QStringList list; + int tail = 0; + int head = 0; + while ((head = token_regexp.indexIn(text, head)) != -1) + { + result.append(text.mid(tail, head - tail)); + QString key = token_regexp.cap(1); + auto iter = with.find(key); + if (iter != with.end()) + { + result.append(*iter); + } + head += token_regexp.matchedLength(); + tail = head; + } + result.append(text.mid(tail)); + return result; +} + +QStringList OneSixInstance::processMinecraftArgs(AuthSessionPtr session) +{ + QString args_pattern = m_version->minecraftArguments; + for (auto tweaker : m_version->tweakers) + { + args_pattern += " --tweakClass " + tweaker; + } + + QMap<QString, QString> token_mapping; + // yggdrasil! + token_mapping["auth_username"] = session->username; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; + + // blatant self-promotion. + token_mapping["profile_name"] = token_mapping["version_name"] = "MultiMC5"; + + QString absRootDir = QDir(minecraftRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + token_mapping["game_assets"] = AssetsUtils::reconstructAssets(m_version->assets).absolutePath(); + + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = m_version->assets; + + QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); + for (int i = 0; i < parts.length(); i++) + { + parts[i] = replaceTokensIn(parts[i], token_mapping); + } + return parts; +} + +BaseProcess *OneSixInstance::prepareForLaunch(AuthSessionPtr session) +{ + QString launchScript; + QIcon icon = ENV.icons()->getIcon(iconKey()); + auto pixmap = icon.pixmap(128, 128); + pixmap.save(PathCombine(minecraftRoot(), "icon.png"), "PNG"); + + if (!m_version) + return nullptr; + + // libraries and class path. + { + auto libs = m_version->getActiveNormalLibs(); + for (auto lib : libs) + { + launchScript += "cp " + librariesPath().absoluteFilePath(lib->storagePath()) + "\n"; + } + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + launchScript += "cp " + QDir(instanceRoot()).absoluteFilePath("temp.jar") + "\n"; + } + else + { + QString relpath = m_version->id + "/" + m_version->id + ".jar"; + launchScript += "cp " + versionsPath().absoluteFilePath(relpath) + "\n"; + } + } + if (!m_version->mainClass.isEmpty()) + { + launchScript += "mainClass " + m_version->mainClass + "\n"; + } + if (!m_version->appletClass.isEmpty()) + { + launchScript += "appletClass " + m_version->appletClass + "\n"; + } + + // generic minecraft params + for (auto param : processMinecraftArgs(session)) + { + launchScript += "param " + param + "\n"; + } + + // window size, title and state, legacy + { + QString windowParams; + if (settings().get("LaunchMaximized").toBool()) + windowParams = "max"; + else + windowParams = QString("%1x%2") + .arg(settings().get("MinecraftWinWidth").toInt()) + .arg(settings().get("MinecraftWinHeight").toInt()); + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + } + + // legacy auth + { + launchScript += "userName " + session->player_name + "\n"; + launchScript += "sessionId " + session->session + "\n"; + } + + // native libraries (mostly LWJGL) + { + QDir natives_dir(PathCombine(instanceRoot(), "natives/")); + for (auto native : m_version->getActiveNativeLibs()) + { + QFileInfo finfo(PathCombine("libraries", native->storagePath())); + launchScript += "ext " + finfo.absoluteFilePath() + "\n"; + } + launchScript += "natives " + natives_dir.absolutePath() + "\n"; + } + + // traits. including legacyLaunch and others ;) + for (auto trait : m_version->traits) + { + launchScript += "traits " + trait + "\n"; + } + launchScript += "launcher onesix\n"; + + auto process = MinecraftProcess::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr())); + process->setLaunchScript(launchScript); + process->setWorkdir(minecraftRoot()); + process->setLogin(session); + return process; +} + +void OneSixInstance::cleanupAfterRun() +{ + QString target_dir = PathCombine(instanceRoot(), "natives/"); + QDir dir(target_dir); + dir.removeRecursively(); +} + +std::shared_ptr<ModList> OneSixInstance::loaderModList() const +{ + if (!m_loader_mod_list) + { + m_loader_mod_list.reset(new ModList(loaderModsDir())); + } + m_loader_mod_list->update(); + return m_loader_mod_list; +} + +std::shared_ptr<ModList> OneSixInstance::coreModList() const +{ + if (!m_core_mod_list) + { + m_core_mod_list.reset(new ModList(coreModsDir())); + } + m_core_mod_list->update(); + return m_core_mod_list; +} + +std::shared_ptr<ModList> OneSixInstance::resourcePackList() const +{ + if (!m_resource_pack_list) + { + m_resource_pack_list.reset(new ModList(resourcePacksDir())); + } + m_resource_pack_list->update(); + return m_resource_pack_list; +} + +std::shared_ptr<ModList> OneSixInstance::texturePackList() const +{ + if (!m_texture_pack_list) + { + m_texture_pack_list.reset(new ModList(texturePacksDir())); + } + m_texture_pack_list->update(); + return m_texture_pack_list; +} + +bool OneSixInstance::setIntendedVersionId(QString version) +{ + settings().set("IntendedVersion", version); + if(getMinecraftProfile()) + { + clearProfile(); + } + return true; +} + +QList< Mod > OneSixInstance::getJarMods() const +{ + QList<Mod> mods; + for (auto jarmod : m_version->jarMods) + { + QString filePath = jarmodsPath().absoluteFilePath(jarmod->name); + mods.push_back(Mod(QFileInfo(filePath))); + } + return mods; +} + + +QString OneSixInstance::intendedVersionId() const +{ + return settings().get("IntendedVersion").toString(); +} + +void OneSixInstance::setShouldUpdate(bool) +{ +} + +bool OneSixInstance::shouldUpdate() const +{ + return true; +} + +QString OneSixInstance::currentVersionId() const +{ + return intendedVersionId(); +} + +void OneSixInstance::reloadProfile() +{ + try + { + m_version->reload(); + unsetFlag(VersionBrokenFlag); + emit versionReloaded(); + } + catch (VersionIncomplete &error) + { + } + catch (MMCError &error) + { + m_version->clear(); + setFlag(VersionBrokenFlag); + // TODO: rethrow to show some error message(s)? + emit versionReloaded(); + throw; + } +} + +void OneSixInstance::clearProfile() +{ + m_version->clear(); + emit versionReloaded(); +} + +std::shared_ptr<MinecraftProfile> OneSixInstance::getMinecraftProfile() const +{ + return m_version; +} + +QString OneSixInstance::getStatusbarDescription() +{ + QStringList traits; + if (flags() & VersionBrokenFlag) + { + traits.append(tr("broken")); + } + + if (traits.size()) + { + return tr("Minecraft %1 (%2)").arg(intendedVersionId()).arg(traits.join(", ")); + } + else + { + return tr("Minecraft %1").arg(intendedVersionId()); + } +} + +QDir OneSixInstance::librariesPath() const +{ + return QDir::current().absoluteFilePath("libraries"); +} + +QDir OneSixInstance::jarmodsPath() const +{ + return QDir(jarModsDir()); +} + +QDir OneSixInstance::versionsPath() const +{ + return QDir::current().absoluteFilePath("versions"); +} + +bool OneSixInstance::providesVersionFile() const +{ + return false; +} + +bool OneSixInstance::reload() +{ + if (BaseInstance::reload()) + { + try + { + reloadProfile(); + return true; + } + catch (...) + { + return false; + } + } + return false; +} + +QString OneSixInstance::loaderModsDir() const +{ + return PathCombine(minecraftRoot(), "mods"); +} + +QString OneSixInstance::coreModsDir() const +{ + return PathCombine(minecraftRoot(), "coremods"); +} + +QString OneSixInstance::resourcePacksDir() const +{ + return PathCombine(minecraftRoot(), "resourcepacks"); +} + +QString OneSixInstance::texturePacksDir() const +{ + return PathCombine(minecraftRoot(), "texturepacks"); +} + +QString OneSixInstance::instanceConfigFolder() const +{ + return PathCombine(minecraftRoot(), "config"); +} + +QString OneSixInstance::jarModsDir() const +{ + return PathCombine(instanceRoot(), "jarmods"); +} + +QString OneSixInstance::libDir() const +{ + return PathCombine(minecraftRoot(), "lib"); +} + +QStringList OneSixInstance::extraArguments() const +{ + auto list = BaseInstance::extraArguments(); + auto version = getMinecraftProfile(); + if (!version) + return list; + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true", + "-Dfml.ignorePatchDiscrepancies=true"}); + } + return list; +} + +std::shared_ptr<OneSixInstance> OneSixInstance::getSharedPtr() +{ + return std::dynamic_pointer_cast<OneSixInstance>(BaseInstance::getSharedPtr()); +} diff --git a/logic/minecraft/OneSixInstance.h b/logic/minecraft/OneSixInstance.h new file mode 100644 index 00000000..3a9e528e --- /dev/null +++ b/logic/minecraft/OneSixInstance.h @@ -0,0 +1,109 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "logic/minecraft/MinecraftInstance.h" + +#include "logic/minecraft/MinecraftProfile.h" +#include "logic/minecraft/ModList.h" +#include "gui/pages/BasePageProvider.h" + +class OneSixInstance : public MinecraftInstance, public BasePageProvider +{ + Q_OBJECT +public: + explicit OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual ~OneSixInstance(){}; + + virtual void init(); + + ////// Edit Instance Dialog stuff ////// + virtual QList<BasePage *> getPages(); + virtual QString dialogTitle(); + + ////// Mod Lists ////// + std::shared_ptr<ModList> loaderModList() const; + std::shared_ptr<ModList> coreModList() const; + std::shared_ptr<ModList> resourcePackList() const override; + std::shared_ptr<ModList> texturePackList() const override; + virtual QList<Mod> getJarMods() const override; + virtual void createProfile(); + + virtual QSet<QString> traits(); + + ////// Directories and files ////// + QString jarModsDir() const; + QString resourcePacksDir() const; + QString texturePacksDir() const; + QString loaderModsDir() const; + QString coreModsDir() const; + QString libDir() const; + virtual QString instanceConfigFolder() const override; + + virtual std::shared_ptr<Task> doUpdate() override; + virtual BaseProcess *prepareForLaunch(AuthSessionPtr account) override; + + virtual void cleanupAfterRun() override; + + virtual QString intendedVersionId() const override; + virtual bool setIntendedVersionId(QString version) override; + + virtual QString currentVersionId() const override; + + virtual bool shouldUpdate() const override; + virtual void setShouldUpdate(bool val) override; + + /** + * reload the profile, including version json files. + * + * throws various exceptions :3 + */ + void reloadProfile(); + + /// clears all version information in preparation for an update + void clearProfile(); + + /// get the current full version info + std::shared_ptr<MinecraftProfile> getMinecraftProfile() const; + + virtual QString getStatusbarDescription() override; + + virtual QDir jarmodsPath() const; + virtual QDir librariesPath() const; + virtual QDir versionsPath() const; + virtual bool providesVersionFile() const; + + bool reload() override; + + virtual QStringList extraArguments() const override; + + std::shared_ptr<OneSixInstance> getSharedPtr(); + +signals: + void versionReloaded(); + +private: + QStringList processMinecraftArgs(AuthSessionPtr account); + +protected: + std::shared_ptr<MinecraftProfile> m_version; + mutable std::shared_ptr<ModList> m_loader_mod_list; + mutable std::shared_ptr<ModList> m_core_mod_list; + mutable std::shared_ptr<ModList> m_resource_pack_list; + mutable std::shared_ptr<ModList> m_texture_pack_list; +}; + +Q_DECLARE_METATYPE(std::shared_ptr<OneSixInstance>) diff --git a/logic/minecraft/OneSixProfileStrategy.cpp b/logic/minecraft/OneSixProfileStrategy.cpp index 9fcd0336..6d80963f 100644 --- a/logic/minecraft/OneSixProfileStrategy.cpp +++ b/logic/minecraft/OneSixProfileStrategy.cpp @@ -1,6 +1,6 @@ #include "logic/minecraft/OneSixProfileStrategy.h" #include "logic/minecraft/VersionBuildError.h" -#include "logic/OneSixInstance.h" +#include "logic/minecraft/OneSixInstance.h" #include "logic/minecraft/MinecraftVersionList.h" #include "logic/Env.h" diff --git a/logic/minecraft/OneSixUpdate.cpp b/logic/minecraft/OneSixUpdate.cpp new file mode 100644 index 00000000..f9ab6c65 --- /dev/null +++ b/logic/minecraft/OneSixUpdate.cpp @@ -0,0 +1,445 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "logic/Env.h" +#include "OneSixUpdate.h" + +#include <QtNetwork> + +#include <QFile> +#include <QFileInfo> +#include <QTextStream> +#include <QDataStream> +#include <pathutils.h> +#include <JlCompress.h> + +#include "logic/BaseInstance.h" +#include "logic/minecraft/MinecraftVersionList.h" +#include "logic/minecraft/MinecraftProfile.h" +#include "logic/minecraft/OneSixLibrary.h" +#include "logic/minecraft/OneSixInstance.h" +#include "logic/forge/ForgeMirrors.h" +#include "logic/net/URLConstants.h" +#include "logic/minecraft/AssetsUtils.h" +#include "logic/minecraft/JarUtils.h" + +OneSixUpdate::OneSixUpdate(OneSixInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void OneSixUpdate::executeTask() +{ + // Make directories + QDir mcDir(m_inst->minecraftRoot()); + if (!mcDir.exists() && !mcDir.mkpath(".")) + { + emitFailed(tr("Failed to create folder for minecraft binaries.")); + return; + } + + // Get a pointer to the version object that corresponds to the instance's version. + targetVersion = std::dynamic_pointer_cast<MinecraftVersion>( + ENV.getVersion("net.minecraft", m_inst->intendedVersionId())); + if (targetVersion == nullptr) + { + // don't do anything if it was invalid + emitFailed(tr("The specified Minecraft version is invalid. Choose a different one.")); + return; + } + if (m_inst->providesVersionFile() || !targetVersion->needsUpdate()) + { + qDebug() << "Instance either provides a version file or doesn't need an update."; + jarlibStart(); + return; + } + versionUpdateTask = std::dynamic_pointer_cast<MinecraftVersionList>(ENV.getVersionList("net.minecraft"))->createUpdateTask(m_inst->intendedVersionId()); + if (!versionUpdateTask) + { + qDebug() << "Didn't spawn an update task."; + jarlibStart(); + return; + } + connect(versionUpdateTask.get(), SIGNAL(succeeded()), SLOT(jarlibStart())); + connect(versionUpdateTask.get(), SIGNAL(failed(QString)), SLOT(versionUpdateFailed(QString))); + connect(versionUpdateTask.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + setStatus(tr("Getting the version files from Mojang...")); + versionUpdateTask->start(); +} + +void OneSixUpdate::versionUpdateFailed(QString reason) +{ + emitFailed(reason); +} + +void OneSixUpdate::assetIndexStart() +{ + setStatus(tr("Updating assets index...")); + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<MinecraftProfile> version = inst->getMinecraftProfile(); + QString assetName = version->assets; + QUrl indexUrl = "http://" + URLConstants::AWS_DOWNLOAD_INDEXES + assetName + ".json"; + QString localPath = assetName + ".json"; + auto job = new NetJob(tr("Asset index for %1").arg(inst->name())); + + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("asset_indexes", localPath); + job->addNetAction(CacheDownload::make(indexUrl, entry)); + jarlibDownloadJob.reset(job); + + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetIndexFinished())); + connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(assetIndexFailed())); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + + jarlibDownloadJob->start(); +} + +void OneSixUpdate::assetIndexFinished() +{ + AssetsIndex index; + + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<MinecraftProfile> version = inst->getMinecraftProfile(); + QString assetName = version->assets; + + QString asset_fname = "assets/indexes/" + assetName + ".json"; + if (!AssetsUtils::loadAssetsIndexJson(asset_fname, &index)) + { + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("asset_indexes", assetName + ".json"); + metacache->evictEntry(entry); + emitFailed(tr("Failed to read the assets index!")); + } + + QList<Md5EtagDownloadPtr> dls; + for (auto object : index.objects.values()) + { + QString objectName = object.hash.left(2) + "/" + object.hash; + QFileInfo objectFile("assets/objects/" + objectName); + if ((!objectFile.isFile()) || (objectFile.size() != object.size)) + { + auto objectDL = MD5EtagDownload::make( + QUrl("http://" + URLConstants::RESOURCE_BASE + objectName), + objectFile.filePath()); + objectDL->m_total_progress = object.size; + dls.append(objectDL); + } + } + if (dls.size()) + { + setStatus(tr("Getting the assets files from Mojang...")); + auto job = new NetJob(tr("Assets for %1").arg(inst->name())); + for (auto dl : dls) + job->addNetAction(dl); + jarlibDownloadJob.reset(job); + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetsFinished())); + connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(assetsFailed())); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + jarlibDownloadJob->start(); + return; + } + assetsFinished(); +} + +void OneSixUpdate::assetIndexFailed() +{ + emitFailed(tr("Failed to download the assets index!")); +} + +void OneSixUpdate::assetsFinished() +{ + emitSucceeded(); +} + +void OneSixUpdate::assetsFailed() +{ + emitFailed(tr("Failed to download assets!")); +} + +void OneSixUpdate::jarlibStart() +{ + setStatus(tr("Getting the library files from Mojang...")); + qDebug() << m_inst->name() << ": downloading libraries"; + OneSixInstance *inst = (OneSixInstance *)m_inst; + try + { + inst->reloadProfile(); + } + catch (MMCError &e) + { + emitFailed(e.cause()); + return; + } + catch (...) + { + emitFailed(tr("Failed to load the version description file for reasons unknown.")); + return; + } + + // Build a list of URLs that will need to be downloaded. + std::shared_ptr<MinecraftProfile> version = inst->getMinecraftProfile(); + // minecraft.jar for this version + { + QString version_id = version->id; + QString localPath = version_id + "/" + version_id + ".jar"; + QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath; + + auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); + + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + job->addNetAction(CacheDownload::make(QUrl(urlstr), entry)); + jarHashOnEntry = entry->md5sum; + + jarlibDownloadJob.reset(job); + } + + auto libs = version->getActiveNativeLibs(); + libs.append(version->getActiveNormalLibs()); + + auto metacache = ENV.metacache(); + QList<ForgeXzDownloadPtr> ForgeLibs; + QList<std::shared_ptr<OneSixLibrary>> brokenLocalLibs; + + for (auto lib : libs) + { + if (lib->hint() == "local") + { + if (!lib->filesExist(m_inst->librariesPath())) + brokenLocalLibs.append(lib); + continue; + } + + QString raw_storage = lib->storagePath(); + QString raw_dl = lib->downloadUrl(); + + auto f = [&](QString storage, QString dl) + { + auto entry = metacache->resolveEntry("libraries", storage); + if (entry->stale) + { + if (lib->hint() == "forge-pack-xz") + { + ForgeLibs.append(ForgeXzDownload::make(storage, entry)); + } + else + { + jarlibDownloadJob->addNetAction(CacheDownload::make(dl, entry)); + } + } + }; + if (raw_storage.contains("${arch}")) + { + QString cooked_storage = raw_storage; + QString cooked_dl = raw_dl; + f(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32")); + cooked_storage = raw_storage; + cooked_dl = raw_dl; + f(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64")); + } + else + { + f(raw_storage, raw_dl); + } + } + if (!brokenLocalLibs.empty()) + { + jarlibDownloadJob.reset(); + QStringList failed; + for (auto brokenLib : brokenLocalLibs) + { + failed.append(brokenLib->files()); + } + QString failed_all = failed.join("\n"); + emitFailed(tr("Some libraries marked as 'local' are missing their jar " + "files:\n%1\n\nYou'll have to correct this problem manually. If this is " + "an externally tracked instance, make sure to run it at least once " + "outside of MultiMC.").arg(failed_all)); + return; + } + // TODO: think about how to propagate this from the original json file... or IF AT ALL + QString forgeMirrorList = "http://files.minecraftforge.net/mirror-brand.list"; + if (!ForgeLibs.empty()) + { + jarlibDownloadJob->addNetAction( + ForgeMirrors::make(ForgeLibs, jarlibDownloadJob, forgeMirrorList)); + } + + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(jarlibFinished())); + connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(jarlibFailed())); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + + jarlibDownloadJob->start(); +} + +void OneSixUpdate::jarlibFinished() +{ + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<MinecraftProfile> version = inst->getMinecraftProfile(); + + // nuke obsolete stripped jar(s) if needed + QString version_id = version->id; + QString strippedPath = version_id + "/" + version_id + "-stripped.jar"; + QFile strippedJar(strippedPath); + if(strippedJar.exists()) + { + strippedJar.remove(); + } + auto finalJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("temp.jar"); + QFile finalJar(finalJarPath); + if(finalJar.exists()) + { + if(!finalJar.remove()) + { + emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); + return; + } + } + + // create temporary modded jar, if needed + auto jarMods = inst->getJarMods(); + if(jarMods.size()) + { + auto sourceJarPath = m_inst->versionsPath().absoluteFilePath(version->id + "/" + version->id + ".jar"); + QString localPath = version_id + "/" + version_id + ".jar"; + auto metacache = ENV.metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + QString fullJarPath = entry->getFullPath(); + if(!JarUtils::createModdedJar(sourceJarPath, finalJarPath, jarMods)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + } + if (version->traits.contains("legacyFML")) + { + fmllibsStart(); + } + else + { + assetIndexStart(); + } +} + +void OneSixUpdate::jarlibFailed() +{ + QStringList failed = jarlibDownloadJob->getFailedFiles(); + QString failed_all = failed.join("\n"); + emitFailed( + tr("Failed to download the following files:\n%1\n\nPlease try again.").arg(failed_all)); +} + +void OneSixUpdate::fmllibsStart() +{ + // Get the mod list + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<MinecraftProfile> fullversion = inst->getMinecraftProfile(); + bool forge_present = false; + + QString version = inst->intendedVersionId(); + auto &fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + assetIndexStart(); + return; + } + + auto &libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + forge_present = (fullversion->versionPatch("net.minecraftforge") != nullptr); + // we don't... + if (!forge_present) + { + assetIndexStart(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto &lib : libList) + { + QFileInfo libInfo(PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + assetIndexStart(); + return; + } + + // download missing libs to our place + setStatus(tr("Dowloading FML libraries...")); + auto dljob = new NetJob("FML libraries"); + auto metacache = ENV.metacache(); + for (auto &lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename + : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; + dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry)); + } + + connect(dljob, SIGNAL(succeeded()), SLOT(fmllibsFinished())); + connect(dljob, SIGNAL(failed()), SLOT(fmllibsFailed())); + connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + legacyDownloadJob.reset(dljob); + legacyDownloadJob->start(); +} + +void OneSixUpdate::fmllibsFinished() +{ + legacyDownloadJob.reset(); + if (!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + OneSixInstance *inst = (OneSixInstance *)m_inst; + auto metacache = ENV.metacache(); + int index = 0; + for (auto &lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = PathCombine(inst->libDir(), lib.filename); + if (!ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + assetIndexStart(); +} + +void OneSixUpdate::fmllibsFailed() +{ + emitFailed("Game update failed: it was impossible to fetch the required FML libraries."); + return; +} + diff --git a/logic/minecraft/OneSixUpdate.h b/logic/minecraft/OneSixUpdate.h new file mode 100644 index 00000000..e3571e5a --- /dev/null +++ b/logic/minecraft/OneSixUpdate.h @@ -0,0 +1,68 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QObject> +#include <QList> +#include <QUrl> + +#include "logic/net/NetJob.h" +#include "logic/tasks/Task.h" +#include "logic/minecraft/VersionFilterData.h" +#include <quazip.h> + +class MinecraftVersion; +class OneSixInstance; + +class OneSixUpdate : public Task +{ + Q_OBJECT +public: + explicit OneSixUpdate(OneSixInstance *inst, QObject *parent = 0); + virtual void executeTask(); + +private +slots: + void versionUpdateFailed(QString reason); + + void jarlibStart(); + void jarlibFinished(); + void jarlibFailed(); + + void fmllibsStart(); + void fmllibsFinished(); + void fmllibsFailed(); + + void assetIndexStart(); + void assetIndexFinished(); + void assetIndexFailed(); + + void assetsFinished(); + void assetsFailed(); + +private: + NetJobPtr jarlibDownloadJob; + NetJobPtr legacyDownloadJob; + + /// target version, determined during this task + std::shared_ptr<MinecraftVersion> targetVersion; + /// the task that is spawned for version updates + std::shared_ptr<Task> versionUpdateTask; + + OneSixInstance *m_inst = nullptr; + QString jarHashOnEntry; + QList<FMLlib> fmlLibsToProcess; +}; diff --git a/logic/minecraft/SkinUtils.cpp b/logic/minecraft/SkinUtils.cpp new file mode 100644 index 00000000..f44c9c70 --- /dev/null +++ b/logic/minecraft/SkinUtils.cpp @@ -0,0 +1,47 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "logic/minecraft/SkinUtils.h" +#include "logic/net/HttpMetaCache.h" +#include "logic/Env.h" + +#include <QFile> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> + +namespace SkinUtils +{ +/* + * Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise + */ +QPixmap getFaceFromCache(QString username, int height, int width) +{ + QFile fskin(ENV.metacache() + ->resolveEntry("skins", username + ".png") + ->getFullPath()); + + if (fskin.exists()) + { + QPixmap skin(fskin.fileName()); + if(!skin.isNull()) + { + return skin.copy(8, 8, 8, 8).scaled(height, width, Qt::KeepAspectRatio); + } + } + + return QPixmap(); +} +} diff --git a/logic/minecraft/SkinUtils.h b/logic/minecraft/SkinUtils.h new file mode 100644 index 00000000..bab9d45e --- /dev/null +++ b/logic/minecraft/SkinUtils.h @@ -0,0 +1,23 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QPixmap> + +namespace SkinUtils +{ +QPixmap getFaceFromCache(QString username, int height = 64, int width = 64); +} diff --git a/logic/minecraft/VersionBuilder.cpp b/logic/minecraft/VersionBuilder.cpp index f3a0d490..bc29c187 100644 --- a/logic/minecraft/VersionBuilder.cpp +++ b/logic/minecraft/VersionBuilder.cpp @@ -35,7 +35,7 @@ #include "MinecraftVersionList.h" #include "ProfileUtils.h" -#include "logic/OneSixInstance.h" +#include "logic/minecraft/OneSixInstance.h" #include "logic/MMCJson.h" #include <QDebug> |