From 17ad1e64f824fba6d8f153191effdb2af7d387c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 27 Feb 2016 19:58:40 +0100 Subject: NOISSUE move files into paths that make more sense --- logic/minecraft/LegacyInstance.cpp | 455 ---------------- logic/minecraft/LegacyInstance.h | 135 ----- logic/minecraft/LegacyUpdate.cpp | 395 -------------- logic/minecraft/LegacyUpdate.h | 70 --- logic/minecraft/LwjglVersionList.cpp | 189 ------- logic/minecraft/LwjglVersionList.h | 156 ------ logic/minecraft/OneSixInstance.cpp | 598 --------------------- logic/minecraft/OneSixInstance.h | 110 ---- logic/minecraft/OneSixProfileStrategy.cpp | 413 -------------- logic/minecraft/OneSixProfileStrategy.h | 26 - logic/minecraft/OneSixUpdate.cpp | 411 -------------- logic/minecraft/OneSixUpdate.h | 68 --- logic/minecraft/RawLibrary.h | 2 +- logic/minecraft/auth/AuthSession.cpp | 30 ++ logic/minecraft/auth/AuthSession.h | 51 ++ logic/minecraft/auth/MojangAccount.cpp | 278 ++++++++++ logic/minecraft/auth/MojangAccount.h | 173 ++++++ logic/minecraft/auth/MojangAccountList.cpp | 427 +++++++++++++++ logic/minecraft/auth/MojangAccountList.h | 201 +++++++ logic/minecraft/auth/YggdrasilTask.cpp | 255 +++++++++ logic/minecraft/auth/YggdrasilTask.h | 150 ++++++ logic/minecraft/auth/flows/AuthenticateTask.cpp | 202 +++++++ logic/minecraft/auth/flows/AuthenticateTask.h | 46 ++ logic/minecraft/auth/flows/RefreshTask.cpp | 144 +++++ logic/minecraft/auth/flows/RefreshTask.h | 44 ++ logic/minecraft/auth/flows/ValidateTask.cpp | 61 +++ logic/minecraft/auth/flows/ValidateTask.h | 47 ++ logic/minecraft/forge/ForgeInstaller.cpp | 442 +++++++++++++++ logic/minecraft/forge/ForgeInstaller.h | 52 ++ logic/minecraft/forge/ForgeMirror.h | 10 + logic/minecraft/forge/ForgeMirrors.cpp | 118 ++++ logic/minecraft/forge/ForgeMirrors.h | 61 +++ logic/minecraft/forge/ForgeVersion.cpp | 55 ++ logic/minecraft/forge/ForgeVersion.h | 42 ++ logic/minecraft/forge/ForgeVersionList.cpp | 450 ++++++++++++++++ logic/minecraft/forge/ForgeVersionList.h | 90 ++++ logic/minecraft/forge/ForgeXzDownload.cpp | 389 ++++++++++++++ logic/minecraft/forge/ForgeXzDownload.h | 66 +++ logic/minecraft/forge/LegacyForge.cpp | 56 ++ logic/minecraft/forge/LegacyForge.h | 25 + logic/minecraft/ftb/FTBPlugin.cpp | 395 ++++++++++++++ logic/minecraft/ftb/FTBPlugin.h | 13 + logic/minecraft/ftb/FTBProfileStrategy.cpp | 128 +++++ logic/minecraft/ftb/FTBProfileStrategy.h | 21 + logic/minecraft/ftb/FTBVersion.h | 32 ++ logic/minecraft/ftb/LegacyFTBInstance.cpp | 27 + logic/minecraft/ftb/LegacyFTBInstance.h | 13 + logic/minecraft/ftb/OneSixFTBInstance.cpp | 138 +++++ logic/minecraft/ftb/OneSixFTBInstance.h | 27 + logic/minecraft/legacy/LegacyInstance.cpp | 455 ++++++++++++++++ logic/minecraft/legacy/LegacyInstance.h | 135 +++++ logic/minecraft/legacy/LegacyUpdate.cpp | 395 ++++++++++++++ logic/minecraft/legacy/LegacyUpdate.h | 70 +++ logic/minecraft/legacy/LwjglVersionList.cpp | 189 +++++++ logic/minecraft/legacy/LwjglVersionList.h | 156 ++++++ logic/minecraft/liteloader/LiteLoaderInstaller.cpp | 141 +++++ logic/minecraft/liteloader/LiteLoaderInstaller.h | 39 ++ .../minecraft/liteloader/LiteLoaderVersionList.cpp | 275 ++++++++++ logic/minecraft/liteloader/LiteLoaderVersionList.h | 119 ++++ logic/minecraft/onesix/OneSixInstance.cpp | 598 +++++++++++++++++++++ logic/minecraft/onesix/OneSixInstance.h | 110 ++++ logic/minecraft/onesix/OneSixProfileStrategy.cpp | 414 ++++++++++++++ logic/minecraft/onesix/OneSixProfileStrategy.h | 26 + logic/minecraft/onesix/OneSixUpdate.cpp | 411 ++++++++++++++ logic/minecraft/onesix/OneSixUpdate.h | 68 +++ 65 files changed, 8361 insertions(+), 3027 deletions(-) delete mode 100644 logic/minecraft/LegacyInstance.cpp delete mode 100644 logic/minecraft/LegacyInstance.h delete mode 100644 logic/minecraft/LegacyUpdate.cpp delete mode 100644 logic/minecraft/LegacyUpdate.h delete mode 100644 logic/minecraft/LwjglVersionList.cpp delete mode 100644 logic/minecraft/LwjglVersionList.h delete mode 100644 logic/minecraft/OneSixInstance.cpp delete mode 100644 logic/minecraft/OneSixInstance.h delete mode 100644 logic/minecraft/OneSixProfileStrategy.cpp delete mode 100644 logic/minecraft/OneSixProfileStrategy.h delete mode 100644 logic/minecraft/OneSixUpdate.cpp delete mode 100644 logic/minecraft/OneSixUpdate.h create mode 100644 logic/minecraft/auth/AuthSession.cpp create mode 100644 logic/minecraft/auth/AuthSession.h create mode 100644 logic/minecraft/auth/MojangAccount.cpp create mode 100644 logic/minecraft/auth/MojangAccount.h create mode 100644 logic/minecraft/auth/MojangAccountList.cpp create mode 100644 logic/minecraft/auth/MojangAccountList.h create mode 100644 logic/minecraft/auth/YggdrasilTask.cpp create mode 100644 logic/minecraft/auth/YggdrasilTask.h create mode 100644 logic/minecraft/auth/flows/AuthenticateTask.cpp create mode 100644 logic/minecraft/auth/flows/AuthenticateTask.h create mode 100644 logic/minecraft/auth/flows/RefreshTask.cpp create mode 100644 logic/minecraft/auth/flows/RefreshTask.h create mode 100644 logic/minecraft/auth/flows/ValidateTask.cpp create mode 100644 logic/minecraft/auth/flows/ValidateTask.h create mode 100644 logic/minecraft/forge/ForgeInstaller.cpp create mode 100644 logic/minecraft/forge/ForgeInstaller.h create mode 100644 logic/minecraft/forge/ForgeMirror.h create mode 100644 logic/minecraft/forge/ForgeMirrors.cpp create mode 100644 logic/minecraft/forge/ForgeMirrors.h create mode 100644 logic/minecraft/forge/ForgeVersion.cpp create mode 100644 logic/minecraft/forge/ForgeVersion.h create mode 100644 logic/minecraft/forge/ForgeVersionList.cpp create mode 100644 logic/minecraft/forge/ForgeVersionList.h create mode 100644 logic/minecraft/forge/ForgeXzDownload.cpp create mode 100644 logic/minecraft/forge/ForgeXzDownload.h create mode 100644 logic/minecraft/forge/LegacyForge.cpp create mode 100644 logic/minecraft/forge/LegacyForge.h create mode 100644 logic/minecraft/ftb/FTBPlugin.cpp create mode 100644 logic/minecraft/ftb/FTBPlugin.h create mode 100644 logic/minecraft/ftb/FTBProfileStrategy.cpp create mode 100644 logic/minecraft/ftb/FTBProfileStrategy.h create mode 100644 logic/minecraft/ftb/FTBVersion.h create mode 100644 logic/minecraft/ftb/LegacyFTBInstance.cpp create mode 100644 logic/minecraft/ftb/LegacyFTBInstance.h create mode 100644 logic/minecraft/ftb/OneSixFTBInstance.cpp create mode 100644 logic/minecraft/ftb/OneSixFTBInstance.h create mode 100644 logic/minecraft/legacy/LegacyInstance.cpp create mode 100644 logic/minecraft/legacy/LegacyInstance.h create mode 100644 logic/minecraft/legacy/LegacyUpdate.cpp create mode 100644 logic/minecraft/legacy/LegacyUpdate.h create mode 100644 logic/minecraft/legacy/LwjglVersionList.cpp create mode 100644 logic/minecraft/legacy/LwjglVersionList.h create mode 100644 logic/minecraft/liteloader/LiteLoaderInstaller.cpp create mode 100644 logic/minecraft/liteloader/LiteLoaderInstaller.h create mode 100644 logic/minecraft/liteloader/LiteLoaderVersionList.cpp create mode 100644 logic/minecraft/liteloader/LiteLoaderVersionList.h create mode 100644 logic/minecraft/onesix/OneSixInstance.cpp create mode 100644 logic/minecraft/onesix/OneSixInstance.h create mode 100644 logic/minecraft/onesix/OneSixProfileStrategy.cpp create mode 100644 logic/minecraft/onesix/OneSixProfileStrategy.h create mode 100644 logic/minecraft/onesix/OneSixUpdate.cpp create mode 100644 logic/minecraft/onesix/OneSixUpdate.h (limited to 'logic/minecraft') diff --git a/logic/minecraft/LegacyInstance.cpp b/logic/minecraft/LegacyInstance.cpp deleted file mode 100644 index 227fd601..00000000 --- a/logic/minecraft/LegacyInstance.cpp +++ /dev/null @@ -1,455 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include - -#include "LegacyInstance.h" - -#include "minecraft/LegacyUpdate.h" -#include "icons/IconList.h" -#include "launch/LaunchTask.h" -#include -#include -#include -#include -#include -#include -#include -#include "minecraft/ModList.h" -#include "minecraft/WorldList.h" -#include -#include - -LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) - : MinecraftInstance(globalSettings, settings, rootDir) -{ - m_lwjglFolderSetting = globalSettings->getSetting("LWJGLDir"); - settings->registerSetting("NeedsRebuild", true); - settings->registerSetting("ShouldUpdate", false); - settings->registerSetting("JarVersion", "Unknown"); - settings->registerSetting("LwjglVersion", "2.9.0"); - settings->registerSetting("IntendedJarVersion", ""); - /* - * custom base jar has no default. it is determined in code... see the accessor methods for - *it - * - * for instances that DO NOT have the CustomBaseJar setting (legacy instances), - * [.]minecraft/bin/mcbackup.jar is the default base jar - */ - settings->registerSetting("UseCustomBaseJar", true); - settings->registerSetting("CustomBaseJar", ""); -} - -QString LegacyInstance::baseJar() const -{ - bool customJar = m_settings->get("UseCustomBaseJar").toBool(); - if (customJar) - { - return customBaseJar(); - } - else - return defaultBaseJar(); -} - -QString LegacyInstance::customBaseJar() const -{ - QString value = m_settings->get("CustomBaseJar").toString(); - if (value.isNull() || value.isEmpty()) - { - return defaultCustomBaseJar(); - } - return value; -} - -void LegacyInstance::setCustomBaseJar(QString val) -{ - if (val.isNull() || val.isEmpty() || val == defaultCustomBaseJar()) - m_settings->reset("CustomBaseJar"); - else - m_settings->set("CustomBaseJar", val); -} - -void LegacyInstance::setShouldUseCustomBaseJar(bool val) -{ - m_settings->set("UseCustomBaseJar", val); -} - -bool LegacyInstance::shouldUseCustomBaseJar() const -{ - return m_settings->get("UseCustomBaseJar").toBool(); -} - - -std::shared_ptr LegacyInstance::createUpdateTask() -{ - // make sure the jar mods list is initialized by asking for it. - auto list = jarModList(); - // create an update task - return std::shared_ptr(new LegacyUpdate(this, this)); -} - -std::shared_ptr LegacyInstance::createLaunchTask(AuthSessionPtr session) -{ - QString launchScript; - QIcon icon = ENV.icons()->getIcon(iconKey()); - auto pixmap = icon.pixmap(128, 128); - pixmap.save(FS::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 " + session->player_name + "\n"; - launchScript += "sessionId " + session->session + "\n"; - launchScript += "windowTitle " + windowTitle() + "\n"; - launchScript += "windowParams " + windowParams + "\n"; - launchScript += "lwjgl " + lwjgl + "\n"; - launchScript += "launcher legacy\n"; - } - auto process = LaunchTask::create(std::dynamic_pointer_cast(getSharedPtr())); - auto pptr = process.get(); - - // print a header - { - process->appendStep(std::make_shared(pptr, "Minecraft folder is:\n" + minecraftRoot() + "\n\n", MessageLevel::MultiMC)); - } - { - auto step = std::make_shared(pptr); - process->appendStep(step); - } - // run pre-launch command if that's needed - if(getPreLaunchCommand().size()) - { - auto step = std::make_shared(pptr); - step->setWorkingDirectory(minecraftRoot()); - process->appendStep(step); - } - // if we aren't in offline mode,. - if(session->status != AuthSession::PlayableOffline) - { - process->appendStep(std::make_shared(pptr)); - } - // if there are any jar mods - if(getJarMods().size()) - { - auto step = std::make_shared(pptr); - process->appendStep(step); - } - // actually launch the game - { - auto step = std::make_shared(pptr); - step->setWorkingDirectory(minecraftRoot()); - step->setLaunchScript(launchScript); - process->appendStep(step); - } - // run post-exit command if that's needed - if(getPostExitCommand().size()) - { - auto step = std::make_shared(pptr); - step->setWorkingDirectory(minecraftRoot()); - process->appendStep(step); - } - if (session) - { - process->setCensorFilter(createCensorFilterFromSession(session)); - } - return process; -} - -std::shared_ptr LegacyInstance::createJarModdingTask() -{ - class JarModTask : public Task - { - public: - explicit JarModTask(std::shared_ptr inst) : Task(nullptr), m_inst(inst) - { - } - virtual void executeTask() - { - if (!m_inst->shouldRebuild()) - { - emitSucceeded(); - return; - } - - // Get the mod list - auto modList = m_inst->getJarMods(); - - QFileInfo runnableJar(m_inst->runnableJar()); - QFileInfo baseJar(m_inst->baseJar()); - bool base_is_custom = m_inst->shouldUseCustomBaseJar(); - - // Nothing to do if there are no jar mods to install, no backup and just the mc jar - if (base_is_custom) - { - // yes, this can happen if the instance only has the runnable jar and not the base jar - // it *could* be assumed that such an instance is vanilla, but that wouldn't be safe - // because that's not something mmc4 guarantees - if (runnableJar.isFile() && !baseJar.exists() && modList.empty()) - { - m_inst->setShouldRebuild(false); - emitSucceeded(); - return; - } - - setStatus(tr("Installing mods: Backing up minecraft.jar ...")); - if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath())) - { - emitFailed("It seems both the active and base jar are gone. A fresh base jar will " - "be used on next run."); - m_inst->setShouldRebuild(true); - m_inst->setShouldUpdate(true); - m_inst->setShouldUseCustomBaseJar(false); - return; - } - } - - if (!baseJar.exists()) - { - emitFailed("The base jar " + baseJar.filePath() + " does not exist"); - return; - } - - if (runnableJar.exists() && !QFile::remove(runnableJar.filePath())) - { - emitFailed("Failed to delete old minecraft.jar"); - return; - } - - setStatus(tr("Installing mods: Opening minecraft.jar ...")); - - QString outputJarPath = runnableJar.filePath(); - QString inputJarPath = baseJar.filePath(); - - if(!MMCZip::createModdedJar(inputJarPath, outputJarPath, modList)) - { - emitFailed(tr("Failed to create the custom Minecraft jar file.")); - return; - } - m_inst->setShouldRebuild(false); - // inst->UpdateVersion(true); - emitSucceeded(); - return; - - } - std::shared_ptr m_inst; - }; - return std::make_shared(std::dynamic_pointer_cast(shared_from_this())); -} - -void LegacyInstance::cleanupAfterRun() -{ - // FIXME: delete the launcher and icons and whatnot. -} - -std::shared_ptr 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 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 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 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 LegacyInstance::texturePackList() const -{ - if (!texture_pack_list) - { - texture_pack_list.reset(new ModList(texturePacksDir())); - } - texture_pack_list->update(); - return texture_pack_list; -} - -std::shared_ptr LegacyInstance::worldList() const -{ - if (!m_world_list) - { - m_world_list.reset(new WorldList(savesDir())); - } - return m_world_list; -} - -QString LegacyInstance::jarModsDir() const -{ - return FS::PathCombine(instanceRoot(), "instMods"); -} - -QString LegacyInstance::binDir() const -{ - return FS::PathCombine(minecraftRoot(), "bin"); -} - -QString LegacyInstance::libDir() const -{ - return FS::PathCombine(minecraftRoot(), "lib"); -} - -QString LegacyInstance::savesDir() const -{ - return FS::PathCombine(minecraftRoot(), "saves"); -} - -QString LegacyInstance::loaderModsDir() const -{ - return FS::PathCombine(minecraftRoot(), "mods"); -} - -QString LegacyInstance::coreModsDir() const -{ - return FS::PathCombine(minecraftRoot(), "coremods"); -} - -QString LegacyInstance::resourceDir() const -{ - return FS::PathCombine(minecraftRoot(), "resources"); -} -QString LegacyInstance::texturePacksDir() const -{ - return FS::PathCombine(minecraftRoot(), "texturepacks"); -} - -QString LegacyInstance::runnableJar() const -{ - return FS::PathCombine(binDir(), "minecraft.jar"); -} - -QString LegacyInstance::modListFile() const -{ - return FS::PathCombine(instanceRoot(), "modlist"); -} - -QString LegacyInstance::instanceConfigFolder() const -{ - return FS::PathCombine(minecraftRoot(), "config"); -} - -bool LegacyInstance::shouldRebuild() const -{ - return m_settings->get("NeedsRebuild").toBool(); -} - -void LegacyInstance::setShouldRebuild(bool val) -{ - m_settings->set("NeedsRebuild", val); -} - -QString LegacyInstance::currentVersionId() const -{ - return m_settings->get("JarVersion").toString(); -} - -QString LegacyInstance::lwjglVersion() const -{ - return m_settings->get("LwjglVersion").toString(); -} - -void LegacyInstance::setLWJGLVersion(QString val) -{ - m_settings->set("LwjglVersion", val); -} - -QString LegacyInstance::intendedVersionId() const -{ - return m_settings->get("IntendedJarVersion").toString(); -} - -bool LegacyInstance::setIntendedVersionId(QString version) -{ - settings()->set("IntendedJarVersion", version); - setShouldUpdate(true); - return true; -} - -bool LegacyInstance::shouldUpdate() const -{ - QVariant var = settings()->get("ShouldUpdate"); - if (!var.isValid() || var.toBool() == false) - { - return intendedVersionId() != currentVersionId(); - } - return true; -} - -void LegacyInstance::setShouldUpdate(bool val) -{ - settings()->set("ShouldUpdate", val); -} - -QString LegacyInstance::defaultBaseJar() const -{ - return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar"; -} - -QString LegacyInstance::defaultCustomBaseJar() const -{ - return FS::PathCombine(binDir(), "mcbackup.jar"); -} - -QString LegacyInstance::lwjglFolder() const -{ - return m_lwjglFolderSetting->get().toString(); -} - -QString LegacyInstance::typeName() const -{ - return tr("Legacy"); -} diff --git a/logic/minecraft/LegacyInstance.h b/logic/minecraft/LegacyInstance.h deleted file mode 100644 index a2ab86de..00000000 --- a/logic/minecraft/LegacyInstance.h +++ /dev/null @@ -1,135 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "minecraft/MinecraftInstance.h" - -#include "multimc_logic_export.h" - -class ModList; -class Task; - -class MULTIMC_LOGIC_EXPORT LegacyInstance : public MinecraftInstance -{ - Q_OBJECT -public: - - explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); - - virtual void init() override {}; - - /// Path to the instance's minecraft.jar - QString runnableJar() const; - - //! Path to the instance's modlist file. - QString modListFile() const; - - /* - ////// Edit Instance Dialog stuff ////// - virtual QList getPages(); - virtual QString dialogTitle(); - */ - - ////// Mod Lists ////// - std::shared_ptr jarModList() const ; - virtual QList< Mod > getJarMods() const override; - std::shared_ptr coreModList() const; - std::shared_ptr loaderModList() const; - std::shared_ptr texturePackList() const override; - std::shared_ptr worldList() const override; - - ////// Directories ////// - QString libDir() const; - QString savesDir() const; - QString texturePacksDir() const; - QString jarModsDir() const; - QString binDir() const; - QString loaderModsDir() const; - QString coreModsDir() const; - QString resourceDir() const; - virtual QString instanceConfigFolder() const override; - - /// Get the curent base jar of this instance. By default, it's the - /// versions/$version/$version.jar - QString baseJar() const; - - /// the default base jar of this instance - QString defaultBaseJar() const; - /// the default custom base jar of this instance - QString defaultCustomBaseJar() const; - - /*! - * Whether or not custom base jar is used - */ - bool shouldUseCustomBaseJar() const; - void setShouldUseCustomBaseJar(bool val); - - /*! - * The value of the custom base jar - */ - QString customBaseJar() const; - void setCustomBaseJar(QString val); - - /*! - * Whether or not the instance's minecraft.jar needs to be rebuilt. - * If this is true, when the instance launches, its jar mods will be - * re-added to a fresh minecraft.jar file. - */ - bool shouldRebuild() const; - void setShouldRebuild(bool val); - - virtual QString currentVersionId() const override; - - //! The version of LWJGL that this instance uses. - QString lwjglVersion() const; - - //! Where the lwjgl versions foor this instance can be found... HACK HACK HACK - QString lwjglFolder() const; - - /// st the version of LWJGL libs this instance will use - void setLWJGLVersion(QString val); - - virtual QString intendedVersionId() const override; - virtual bool setIntendedVersionId(QString version) override; - - virtual QSet traits() override - { - return {"legacy-instance", "texturepacks"}; - }; - - virtual bool shouldUpdate() const override; - virtual void setShouldUpdate(bool val) override; - virtual std::shared_ptr createUpdateTask() override; - - virtual std::shared_ptr createLaunchTask(AuthSessionPtr account) override; - - virtual std::shared_ptr createJarModdingTask() override; - - virtual void cleanupAfterRun() override; - - virtual QString typeName() const override; - -protected: - mutable std::shared_ptr jar_mod_list; - mutable std::shared_ptr core_mod_list; - mutable std::shared_ptr loader_mod_list; - mutable std::shared_ptr texture_pack_list; - mutable std::shared_ptr m_world_list; - std::shared_ptr m_lwjglFolderSetting; -protected -slots: - virtual void jarModsChanged(); -}; diff --git a/logic/minecraft/LegacyUpdate.cpp b/logic/minecraft/LegacyUpdate.cpp deleted file mode 100644 index 2fd8e3cb..00000000 --- a/logic/minecraft/LegacyUpdate.cpp +++ /dev/null @@ -1,395 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include - -#include "Env.h" -#include "BaseInstance.h" -#include "net/URLConstants.h" -#include "MMCZip.h" - -#include "LegacyUpdate.h" - -#include "minecraft/LwjglVersionList.h" -#include "minecraft/MinecraftVersionList.h" -#include "minecraft/ModList.h" -#include "minecraft/LegacyInstance.h" -#include - -LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst) -{ -} - -void LegacyUpdate::executeTask() -{ - fmllibsStart(); -} - -void LegacyUpdate::fmllibsStart() -{ - // Get the mod list - LegacyInstance *inst = (LegacyInstance *)m_inst; - auto modList = inst->jarModList(); - - bool forge_present = false; - - QString version = inst->intendedVersionId(); - auto & fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; - if (!fmlLibsMapping.contains(version)) - { - lwjglStart(); - return; - } - - auto &libList = fmlLibsMapping[version]; - - // determine if we need some libs for FML or forge - setStatus(tr("Checking for FML libraries...")); - for (unsigned i = 0; i < modList->size(); i++) - { - auto &mod = modList->operator[](i); - - // do not use disabled mods. - if (!mod.enabled()) - continue; - - if (mod.type() != Mod::MOD_ZIPFILE) - continue; - - if (mod.mmc_id().contains("forge", Qt::CaseInsensitive)) - { - forge_present = true; - break; - } - if (mod.mmc_id().contains("fml", Qt::CaseInsensitive)) - { - forge_present = true; - break; - } - } - // we don't... - if (!forge_present) - { - lwjglStart(); - return; - } - - // now check the lib folder inside the instance for files. - for (auto &lib : libList) - { - QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); - if (libInfo.exists()) - continue; - fmlLibsToProcess.append(lib); - } - - // if everything is in place, there's nothing to do here... - if (fmlLibsToProcess.isEmpty()) - { - lwjglStart(); - return; - } - - // download missing libs to our place - setStatus(tr("Dowloading FML libraries...")); - auto dljob = new NetJob("FML libraries"); - auto metacache = ENV.metacache(); - for (auto &lib : fmlLibsToProcess) - { - auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename - : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; - dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry)); - } - - connect(dljob, &NetJob::succeeded, this, &LegacyUpdate::fmllibsFinished); - connect(dljob, &NetJob::failed, this, &LegacyUpdate::fmllibsFailed); - connect(dljob, &NetJob::progress, this, &LegacyUpdate::progress); - legacyDownloadJob.reset(dljob); - legacyDownloadJob->start(); -} - -void LegacyUpdate::fmllibsFinished() -{ - legacyDownloadJob.reset(); - if(!fmlLibsToProcess.isEmpty()) - { - setStatus(tr("Copying FML libraries into the instance...")); - LegacyInstance *inst = (LegacyInstance *)m_inst; - auto metacache = ENV.metacache(); - int index = 0; - for (auto &lib : fmlLibsToProcess) - { - progress(index, fmlLibsToProcess.size()); - auto entry = metacache->resolveEntry("fmllibs", lib.filename); - auto path = FS::PathCombine(inst->libDir(), lib.filename); - if(!FS::ensureFilePathExists(path)) - { - emitFailed(tr("Failed creating FML library folder inside the instance.")); - return; - } - if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) - { - emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); - return; - } - index++; - } - progress(index, fmlLibsToProcess.size()); - } - lwjglStart(); -} - -void LegacyUpdate::fmllibsFailed(QString reason) -{ - emitFailed(tr("Game update failed: it was impossible to fetch the required FML libraries. Reason: %1").arg(reason)); - return; -} - -void LegacyUpdate::lwjglStart() -{ - LegacyInstance *inst = (LegacyInstance *)m_inst; - - lwjglVersion = inst->lwjglVersion(); - lwjglTargetPath = FS::PathCombine(inst->lwjglFolder(), lwjglVersion); - lwjglNativesPath = FS::PathCombine(lwjglTargetPath, "natives"); - - // if the 'done' file exists, we don't have to download this again - QFileInfo doneFile(FS::PathCombine(lwjglTargetPath, "done")); - if (doneFile.exists()) - { - jarStart(); - return; - } - - auto list = std::dynamic_pointer_cast(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(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(rep); - connect(rep, &QNetworkReply::downloadProgress, this, &LegacyUpdate::progress); - connect(worker.get(), &QNetworkAccessManager::finished, this, &LegacyUpdate::lwjglFinished); -} - -void LegacyUpdate::lwjglFinished(QNetworkReply *reply) -{ - if (m_reply.get() != reply) - { - return; - } - if (reply->error() != QNetworkReply::NoError) - { - emitFailed("Failed to download: " + reply->errorString() + - "\nSometimes you have to wait a bit if you download many LWJGL versions in " - "a row. YMMV"); - return; - } - auto worker = ENV.qnam(); - // Here i check if there is a cookie for me in the reply and extract it - QList cookies = - qvariant_cast>(reply->header(QNetworkRequest::SetCookieHeader)); - if (cookies.count() != 0) - { - // you must tell which cookie goes with which url - worker->cookieJar()->setCookiesFromUrl(cookies, QUrl("sourceforge.net")); - } - - // here you can check for the 302 or whatever other header i need - QVariant newLoc = reply->header(QNetworkRequest::LocationHeader); - if (newLoc.isValid()) - { - QString redirectedTo = reply->header(QNetworkRequest::LocationHeader).toString(); - QUrl realUrl(redirectedTo); - QString hostname = realUrl.host(); - QNetworkRequest req(redirectedTo); - req.setRawHeader("Host", hostname.toLatin1()); - req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); - QNetworkReply *rep = worker->get(req); - connect(rep, &QNetworkReply::downloadProgress, this, &LegacyUpdate::progress); - m_reply = std::shared_ptr(rep); - return; - } - QFile saveMe("lwjgl.zip"); - saveMe.open(QIODevice::WriteOnly); - saveMe.write(m_reply->readAll()); - saveMe.close(); - setStatus(tr("Installing new LWJGL...")); - extractLwjgl(); - jarStart(); -} -void LegacyUpdate::extractLwjgl() -{ - // make sure the directories are there - - bool success = FS::ensureFolderPathExists(lwjglNativesPath); - - if (!success) - { - emitFailed("Failed to extract the lwjgl libs - error when creating required folders."); - return; - } - - QuaZip zip("lwjgl.zip"); - if (!zip.open(QuaZip::mdUnzip)) - { - emitFailed("Failed to extract the lwjgl libs - not a valid archive."); - return; - } - - // and now we are going to access files inside it - QuaZipFile file(&zip); - const QString jarNames[] = {"jinput.jar", "lwjgl_util.jar", "lwjgl.jar"}; - for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile()) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - emitFailed("Failed to extract the lwjgl libs - error while reading archive."); - return; - } - QuaZipFileInfo info; - QString name = file.getActualFileName(); - if (name.endsWith('/')) - { - file.close(); - continue; - } - QString destFileName; - // Look for the jars - for (int i = 0; i < 3; i++) - { - if (name.endsWith(jarNames[i])) - { - destFileName = FS::PathCombine(lwjglTargetPath, jarNames[i]); - } - } - // Not found? look for the natives - if (destFileName.isEmpty()) - { -#ifdef Q_OS_WIN32 - QString nativesDir = "windows"; -#else -#ifdef Q_OS_MAC - QString nativesDir = "macosx"; -#else - QString nativesDir = "linux"; -#endif -#endif - if (name.contains(nativesDir)) - { - int lastSlash = name.lastIndexOf('/'); - int lastBackSlash = name.lastIndexOf('\\'); - if (lastSlash != -1) - name = name.mid(lastSlash + 1); - else if (lastBackSlash != -1) - name = name.mid(lastBackSlash + 1); - destFileName = FS::PathCombine(lwjglNativesPath, name); - } - } - // Now if destFileName is still empty, go to the next file. - if (!destFileName.isEmpty()) - { - setStatus(tr("Installing new LWJGL - extracting ") + name + "..."); - QFile output(destFileName); - output.open(QIODevice::WriteOnly); - output.write(file.readAll()); - output.close(); - } - file.close(); // do not forget to close! - } - zip.close(); - m_reply.reset(); - QFile doneFile(FS::PathCombine(lwjglTargetPath, "done")); - doneFile.open(QIODevice::WriteOnly); - doneFile.write("done."); - doneFile.close(); -} - -void LegacyUpdate::lwjglFailed(QString reason) -{ - emitFailed(tr("Bad stuff happened while trying to get the lwjgl libs: %1").arg(reason)); -} - -void LegacyUpdate::jarStart() -{ - LegacyInstance *inst = (LegacyInstance *)m_inst; - if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar()) - { - emitSucceeded(); - return; - } - - setStatus(tr("Checking for jar updates...")); - // Make directories - QDir binDir(inst->binDir()); - if (!binDir.exists() && !binDir.mkpath(".")) - { - emitFailed("Failed to create bin folder."); - return; - } - - // Build a list of URLs that will need to be downloaded. - setStatus(tr("Downloading new minecraft.jar ...")); - - QString version_id = inst->intendedVersionId(); - 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(QString)), SLOT(jarFailed(QString))); - connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); - legacyDownloadJob.reset(dljob); - legacyDownloadJob->start(); -} - -void LegacyUpdate::jarFinished() -{ - // process the jar - emitSucceeded(); -} - -void LegacyUpdate::jarFailed(QString reason) -{ - // bad, bad - emitFailed(tr("Failed to download the minecraft jar: %1.").arg(reason)); -} diff --git a/logic/minecraft/LegacyUpdate.h b/logic/minecraft/LegacyUpdate.h deleted file mode 100644 index c52bf934..00000000 --- a/logic/minecraft/LegacyUpdate.h +++ /dev/null @@ -1,70 +0,0 @@ -/* 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 -#include -#include - -#include "net/NetJob.h" -#include "tasks/Task.h" -#include "minecraft/VersionFilterData.h" - -class MinecraftVersion; -class BaseInstance; -class QuaZip; -class Mod; - -class LegacyUpdate : public Task -{ - Q_OBJECT -public: - explicit LegacyUpdate(BaseInstance *inst, QObject *parent = 0); - virtual void executeTask(); - -private -slots: - void lwjglStart(); - void lwjglFinished(QNetworkReply *); - void lwjglFailed(QString reason); - - void jarStart(); - void jarFinished(); - void jarFailed(QString reason); - - void fmllibsStart(); - void fmllibsFinished(); - void fmllibsFailed(QString reason); - - void extractLwjgl(); - -private: - - std::shared_ptr 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 fmlLibsToProcess; -}; diff --git a/logic/minecraft/LwjglVersionList.cpp b/logic/minecraft/LwjglVersionList.cpp deleted file mode 100644 index bb017368..00000000 --- a/logic/minecraft/LwjglVersionList.cpp +++ /dev/null @@ -1,189 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "LwjglVersionList.h" -#include "Env.h" - -#include -#include -#include - -#include - -#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 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(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 deleted file mode 100644 index f043f6e2..00000000 --- a/logic/minecraft/LwjglVersionList.h +++ /dev/null @@ -1,156 +0,0 @@ -/* 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 -#include -#include -#include -#include - -#include "BaseVersion.h" -#include "BaseVersionList.h" - -#include "multimc_logic_export.h" - -class LWJGLVersion; -typedef std::shared_ptr PtrLWJGLVersion; - -class MULTIMC_LOGIC_EXPORT LWJGLVersion : public BaseVersion -{ -public: - LWJGLVersion(const QString &name, const QString &url) - : m_name(name), m_url(url) - { - } - - virtual QString descriptor() - { - return m_name; - } - - virtual QString name() - { - return m_name; - } - - virtual QString typeString() const - { - return QObject::tr("Upstream"); - } - - QString url() const - { - return m_url; - } - -protected: - QString m_name; - QString m_url; -}; - -class MULTIMC_LOGIC_EXPORT LWJGLVersionList : public BaseVersionList -{ - Q_OBJECT -public: - explicit LWJGLVersionList(QObject *parent = 0); - - bool isLoaded() override - { - return m_vlist.length() > 0; - } - virtual const BaseVersionPtr at(int i) const override - { - return m_vlist[i]; - } - - virtual Task* getLoadTask() override - { - return nullptr; - } - - virtual void sortVersions() override {}; - - virtual void updateListData(QList< BaseVersionPtr > versions) override {}; - - int count() const override - { - return m_vlist.length(); - } - - virtual QVariant data(const QModelIndex &index, int role) const override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - virtual int rowCount(const QModelIndex &parent) const override - { - return count(); - } - virtual int columnCount(const QModelIndex &parent) const override; - - virtual bool isLoading() const; - virtual bool errored() const - { - return m_errored; - } - - virtual QString lastErrorMsg() const - { - return m_lastErrorMsg; - } - -public -slots: - /*! - * Loads the version list. - * This is done asynchronously. On success, the loadListFinished() signal will - * be emitted. The list model will be reset as well, resulting in the modelReset() - * signal being emitted. Note that the model will be reset before loadListFinished() is - * emitted. - * If loading the list failed, the loadListFailed(QString msg), - * signal will be emitted. - */ - virtual void loadList(); - -signals: - /*! - * Emitted when the list either starts or finishes loading. - * \param loading Whether or not the list is loading. - */ - void loadingStateUpdated(bool loading); - - void loadListFinished(); - - void loadListFailed(QString msg); - -private: - QList 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/OneSixInstance.cpp b/logic/minecraft/OneSixInstance.cpp deleted file mode 100644 index 9a3920cc..00000000 --- a/logic/minecraft/OneSixInstance.cpp +++ /dev/null @@ -1,598 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include "minecraft/OneSixInstance.h" - -#include "minecraft/OneSixUpdate.h" -#include "minecraft/MinecraftProfile.h" -#include "minecraft/VersionBuildError.h" -#include "launch/LaunchTask.h" -#include -#include -#include -#include -#include -#include -#include -#include "minecraft/OneSixProfileStrategy.h" -#include "MMCZip.h" - -#include "minecraft/AssetsUtils.h" -#include "icons/IconList.h" -#include "minecraft/WorldList.h" -#include - -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))); -} - -QSet OneSixInstance::traits() -{ - auto version = getMinecraftProfile(); - if (!version) - { - return {"version-incomplete"}; - } - else - return version->traits; -} - -std::shared_ptr OneSixInstance::createUpdateTask() -{ - return std::shared_ptr(new OneSixUpdate(this)); -} - -QString replaceTokensIn(QString text, QMap 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 token_mapping; - // yggdrasil! - token_mapping["auth_username"] = session->username; - token_mapping["auth_session"] = session->session; - token_mapping["auth_access_token"] = session->access_token; - token_mapping["auth_player_name"] = session->player_name; - token_mapping["auth_uuid"] = session->uuid; - - // blatant self-promotion. - token_mapping["profile_name"] = token_mapping["version_name"] = "MultiMC5"; - if(m_version->isVanilla()) - { - token_mapping["version_type"] = m_version->type; - } - else - { - token_mapping["version_type"] = "custom"; - } - - 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; - - // 1.9+ version type token - if(m_version->isVanilla()) - { - token_mapping["version_type"] = m_version->type; - } - else - { - token_mapping["version_type"] = "custom"; - } - - QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); - for (int i = 0; i < parts.length(); i++) - { - parts[i] = replaceTokensIn(parts[i], token_mapping); - } - return parts; -} - -std::shared_ptr OneSixInstance::createLaunchTask(AuthSessionPtr session) -{ - QString launchScript; - QIcon icon = ENV.icons()->getIcon(iconKey()); - auto pixmap = icon.pixmap(128, 128); - pixmap.save(FS::PathCombine(minecraftRoot(), "icon.png"), "PNG"); - - if (!m_version) - return nullptr; - - for(auto & mod: loaderModList()->allMods()) - { - if(!mod.enabled()) - continue; - if(mod.type() == Mod::MOD_FOLDER) - continue; - // TODO: proper implementation would need to descend into folders. - - launchScript += "mod " + mod.filename().completeBaseName() + "\n";; - } - - for(auto & coremod: coreModList()->allMods()) - { - if(!coremod.enabled()) - continue; - if(coremod.type() == Mod::MOD_FOLDER) - continue; - // TODO: proper implementation would need to descend into folders. - - launchScript += "coremod " + coremod.filename().completeBaseName() + "\n";; - } - - for(auto & jarmod: m_version->jarMods) - { - launchScript += "jarmod " + jarmod->originalName + " (" + jarmod->name + ")\n"; - } - - // libraries and class path. - { - auto libs = m_version->getActiveNormalLibs(); - for (auto lib : libs) - { - launchScript += "cp " + QFileInfo(lib->storagePath()).absoluteFilePath() + "\n"; - } - auto jarMods = getJarMods(); - if (!jarMods.isEmpty()) - { - launchScript += "cp " + QDir(instanceRoot()).absoluteFilePath("minecraft.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(FS::PathCombine(instanceRoot(), "natives/")); - for (auto native : m_version->getActiveNativeLibs()) - { - QFileInfo finfo(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 = LaunchTask::create(std::dynamic_pointer_cast(getSharedPtr())); - auto pptr = process.get(); - - // print a header - { - process->appendStep(std::make_shared(pptr, "Minecraft folder is:\n" + minecraftRoot() + "\n\n", MessageLevel::MultiMC)); - } - { - auto step = std::make_shared(pptr); - process->appendStep(step); - } - // run pre-launch command if that's needed - if(getPreLaunchCommand().size()) - { - auto step = std::make_shared(pptr); - step->setWorkingDirectory(minecraftRoot()); - process->appendStep(step); - } - // if we aren't in offline mode,. - if(session->status != AuthSession::PlayableOffline) - { - process->appendStep(std::make_shared(pptr)); - } - // if there are any jar mods - if(getJarMods().size()) - { - auto step = std::make_shared(pptr); - process->appendStep(step); - } - // actually launch the game - { - auto step = std::make_shared(pptr); - step->setWorkingDirectory(minecraftRoot()); - step->setLaunchScript(launchScript); - process->appendStep(step); - } - // run post-exit command if that's needed - if(getPostExitCommand().size()) - { - auto step = std::make_shared(pptr); - step->setWorkingDirectory(minecraftRoot()); - process->appendStep(step); - } - if (session) - { - process->setCensorFilter(createCensorFilterFromSession(session)); - } - return process; -} - -std::shared_ptr OneSixInstance::createJarModdingTask() -{ - class JarModTask : public Task - { - public: - explicit JarModTask(std::shared_ptr inst) : Task(nullptr), m_inst(inst) - { - } - virtual void executeTask() - { - std::shared_ptr version = m_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 tempJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("temp.jar"); - QFile tempJar(tempJarPath); - if(tempJar.exists()) - { - tempJar.remove(); - } - auto finalJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("minecraft.jar"); - QFile finalJar(finalJarPath); - if(finalJar.exists()) - { - if(!finalJar.remove()) - { - emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); - return; - } - } - - // create temporary modded jar, if needed - auto jarMods = m_inst->getJarMods(); - if(jarMods.size()) - { - auto sourceJarPath = m_inst->versionsPath().absoluteFilePath(version->id + "/" + version->id + ".jar"); - QString localPath = version_id + "/" + version_id + ".jar"; - auto metacache = ENV.metacache(); - auto entry = metacache->resolveEntry("versions", localPath); - QString fullJarPath = entry->getFullPath(); - if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) - { - emitFailed(tr("Failed to create the custom Minecraft jar file.")); - return; - } - } - emitSucceeded(); - } - std::shared_ptr m_inst; - }; - return std::make_shared(std::dynamic_pointer_cast(shared_from_this())); -} - -void OneSixInstance::cleanupAfterRun() -{ - QString target_dir = FS::PathCombine(instanceRoot(), "natives/"); - QDir dir(target_dir); - dir.removeRecursively(); -} - -std::shared_ptr 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 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 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 OneSixInstance::texturePackList() const -{ - if (!m_texture_pack_list) - { - m_texture_pack_list.reset(new ModList(texturePacksDir())); - } - m_texture_pack_list->update(); - return m_texture_pack_list; -} - -std::shared_ptr OneSixInstance::worldList() const -{ - if (!m_world_list) - { - m_world_list.reset(new WorldList(worldDir())); - } - return m_world_list; -} - -bool OneSixInstance::setIntendedVersionId(QString version) -{ - settings()->set("IntendedVersion", version); - if(getMinecraftProfile()) - { - clearProfile(); - } - emit propertiesChanged(this); - return true; -} - -QList< Mod > OneSixInstance::getJarMods() const -{ - QList 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 (Exception &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 OneSixInstance::getMinecraftProfile() const -{ - return m_version; -} - -QDir OneSixInstance::librariesPath() const -{ - return QDir::current().absoluteFilePath("libraries"); -} - -QDir OneSixInstance::jarmodsPath() const -{ - return QDir(jarModsDir()); -} - -QDir OneSixInstance::versionsPath() const -{ - return QDir::current().absoluteFilePath("versions"); -} - -bool OneSixInstance::providesVersionFile() const -{ - return false; -} - -bool OneSixInstance::reload() -{ - if (BaseInstance::reload()) - { - try - { - reloadProfile(); - return true; - } - catch (...) - { - return false; - } - } - return false; -} - -QString OneSixInstance::loaderModsDir() const -{ - return FS::PathCombine(minecraftRoot(), "mods"); -} - -QString OneSixInstance::coreModsDir() const -{ - return FS::PathCombine(minecraftRoot(), "coremods"); -} - -QString OneSixInstance::resourcePacksDir() const -{ - return FS::PathCombine(minecraftRoot(), "resourcepacks"); -} - -QString OneSixInstance::texturePacksDir() const -{ - return FS::PathCombine(minecraftRoot(), "texturepacks"); -} - -QString OneSixInstance::instanceConfigFolder() const -{ - return FS::PathCombine(minecraftRoot(), "config"); -} - -QString OneSixInstance::jarModsDir() const -{ - return FS::PathCombine(instanceRoot(), "jarmods"); -} - -QString OneSixInstance::libDir() const -{ - return FS::PathCombine(minecraftRoot(), "lib"); -} - -QString OneSixInstance::worldDir() const -{ - return FS::PathCombine(minecraftRoot(), "saves"); -} - -QStringList OneSixInstance::extraArguments() const -{ - auto list = BaseInstance::extraArguments(); - auto version = getMinecraftProfile(); - if (!version) - return list; - auto jarMods = getJarMods(); - if (!jarMods.isEmpty()) - { - list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true", - "-Dfml.ignorePatchDiscrepancies=true"}); - } - return list; -} - -std::shared_ptr OneSixInstance::getSharedPtr() -{ - return std::dynamic_pointer_cast(BaseInstance::getSharedPtr()); -} - -QString OneSixInstance::typeName() const -{ - return tr("OneSix"); -} diff --git a/logic/minecraft/OneSixInstance.h b/logic/minecraft/OneSixInstance.h deleted file mode 100644 index 824e3786..00000000 --- a/logic/minecraft/OneSixInstance.h +++ /dev/null @@ -1,110 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "minecraft/MinecraftInstance.h" - -#include "minecraft/MinecraftProfile.h" -#include "minecraft/ModList.h" - -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT OneSixInstance : public MinecraftInstance -{ - Q_OBJECT -public: - explicit OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); - virtual ~OneSixInstance(){}; - - virtual void init() override; - - ////// Mod Lists ////// - std::shared_ptr loaderModList() const; - std::shared_ptr coreModList() const; - std::shared_ptr resourcePackList() const override; - std::shared_ptr texturePackList() const override; - std::shared_ptr worldList() const override; - virtual QList getJarMods() const override; - virtual void createProfile(); - - virtual QSet traits() override; - - ////// Directories and files ////// - QString jarModsDir() const; - QString resourcePacksDir() const; - QString texturePacksDir() const; - QString loaderModsDir() const; - QString coreModsDir() const; - QString libDir() const; - QString worldDir() const; - virtual QString instanceConfigFolder() const override; - - virtual std::shared_ptr createUpdateTask() override; - virtual std::shared_ptr createLaunchTask(AuthSessionPtr account) override; - virtual std::shared_ptr createJarModdingTask() 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 getMinecraftProfile() const; - - virtual QDir jarmodsPath() const; - virtual QDir librariesPath() const; - virtual QDir versionsPath() const; - virtual bool providesVersionFile() const; - - bool reload() override; - - virtual QStringList extraArguments() const override; - - std::shared_ptr getSharedPtr(); - - virtual QString typeName() const override; - -signals: - void versionReloaded(); - -private: - QStringList processMinecraftArgs(AuthSessionPtr account); - -protected: - std::shared_ptr m_version; - mutable std::shared_ptr m_loader_mod_list; - mutable std::shared_ptr m_core_mod_list; - mutable std::shared_ptr m_resource_pack_list; - mutable std::shared_ptr m_texture_pack_list; - mutable std::shared_ptr m_world_list; -}; - -Q_DECLARE_METATYPE(std::shared_ptr) diff --git a/logic/minecraft/OneSixProfileStrategy.cpp b/logic/minecraft/OneSixProfileStrategy.cpp deleted file mode 100644 index 3e3534a0..00000000 --- a/logic/minecraft/OneSixProfileStrategy.cpp +++ /dev/null @@ -1,413 +0,0 @@ -#include "minecraft/OneSixProfileStrategy.h" -#include "minecraft/VersionBuildError.h" -#include "minecraft/OneSixInstance.h" -#include "minecraft/MinecraftVersionList.h" -#include "Env.h" -#include - -#include -#include -#include -#include - -OneSixProfileStrategy::OneSixProfileStrategy(OneSixInstance* instance) -{ - m_instance = instance; -} - -void OneSixProfileStrategy::upgradeDeprecatedFiles() -{ - auto versionJsonPath = FS::PathCombine(m_instance->instanceRoot(), "version.json"); - auto customJsonPath = FS::PathCombine(m_instance->instanceRoot(), "custom.json"); - auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json"); - - QString sourceFile; - QString renameFile; - - // convert old crap. - if(QFile::exists(customJsonPath)) - { - sourceFile = customJsonPath; - renameFile = versionJsonPath; - } - else if(QFile::exists(versionJsonPath)) - { - sourceFile = versionJsonPath; - } - if(!sourceFile.isEmpty() && !QFile::exists(mcJson)) - { - if(!FS::ensureFilePathExists(mcJson)) - { - qWarning() << "Couldn't create patches folder for" << m_instance->name(); - return; - } - if(!renameFile.isEmpty() && QFile::exists(renameFile)) - { - if(!QFile::rename(renameFile, renameFile + ".old")) - { - qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << m_instance->name(); - return; - } - } - auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false); - ProfileUtils::removeLwjglFromPatch(file); - file->fileId = "net.minecraft"; - file->version = file->id; - file->name = "Minecraft"; - auto data = file->toJson(false).toJson(); - QSaveFile newPatchFile(mcJson); - if(!newPatchFile.open(QIODevice::WriteOnly)) - { - newPatchFile.cancelWriting(); - qWarning() << "Couldn't open main patch for writing in" << m_instance->name(); - return; - } - newPatchFile.write(data); - if(!newPatchFile.commit()) - { - qWarning() << "Couldn't save main patch in" << m_instance->name(); - return; - } - if(!QFile::rename(sourceFile, sourceFile + ".old")) - { - qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << m_instance->name(); - return; - } - } -} - -void OneSixProfileStrategy::loadDefaultBuiltinPatches() -{ - { - auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json"); - // load up the base minecraft patch - ProfilePatchPtr minecraftPatch; - if(QFile::exists(mcJson)) - { - auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false); - if(file->version.isEmpty()) - { - file->version = m_instance->intendedVersionId(); - } - file->setVanilla(false); - file->setRevertible(true); - minecraftPatch = std::dynamic_pointer_cast(file); - } - else - { - auto mcversion = ENV.getVersion("net.minecraft", m_instance->intendedVersionId()); - minecraftPatch = std::dynamic_pointer_cast(mcversion); - } - if (!minecraftPatch) - { - throw VersionIncomplete("net.minecraft"); - } - minecraftPatch->setOrder(-2); - profile->appendPatch(minecraftPatch); - } - - { - auto lwjglJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "org.lwjgl.json"); - ProfilePatchPtr lwjglPatch; - if(QFile::exists(lwjglJson)) - { - auto file = ProfileUtils::parseJsonFile(QFileInfo(lwjglJson), false); - file->setVanilla(false); - file->setRevertible(true); - lwjglPatch = std::dynamic_pointer_cast(file); - } - else - { - // NOTE: this is obviously fake, is fixed in unstable. - QResource LWJGL(":/versions/LWJGL/2.9.1.json"); - auto lwjgl = ProfileUtils::parseJsonFile(LWJGL.absoluteFilePath(), false); - lwjgl->setVanilla(true); - lwjgl->setCustomizable(true); - lwjglPatch = std::dynamic_pointer_cast(lwjgl); - } - if (!lwjglPatch) - { - throw VersionIncomplete("org.lwjgl"); - } - lwjglPatch->setOrder(-1); - profile->appendPatch(lwjglPatch); - } -} - -void OneSixProfileStrategy::loadUserPatches() -{ - // load all patches, put into map for ordering, apply in the right order - ProfileUtils::PatchOrder userOrder; - ProfileUtils::readOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), userOrder); - QDir patches(FS::PathCombine(m_instance->instanceRoot(),"patches")); - QSet seen_extra; - - // first, load things by sort order. - for (auto id : userOrder) - { - // ignore builtins - if (id == "net.minecraft") - continue; - if (id == "org.lwjgl") - continue; - // parse the file - QString filename = patches.absoluteFilePath(id + ".json"); - QFileInfo finfo(filename); - if(!finfo.exists()) - { - qDebug() << "Patch file " << filename << " was deleted by external means..."; - continue; - } - qDebug() << "Reading" << filename << "by user order"; - VersionFilePtr file = ProfileUtils::parseJsonFile(finfo, false); - // sanity check. prevent tampering with files. - if (file->fileId != id) - { - file->addProblem(PROBLEM_WARNING, QObject::tr("load id %1 does not match internal id %2").arg(id, file->fileId)); - seen_extra.insert(file->fileId); - } - file->setRemovable(true); - file->setMovable(true); - profile->appendPatch(file); - } - // now load the rest by internal preference. - QMultiMap files; - for (auto info : patches.entryInfoList(QStringList() << "*.json", QDir::Files)) - { - // parse the file - qDebug() << "Reading" << info.fileName(); - auto file = ProfileUtils::parseJsonFile(info, true); - // ignore builtins - if (file->fileId == "net.minecraft") - continue; - if (file->fileId == "org.lwjgl") - continue; - // do not load versions with broken IDs twice - if(seen_extra.contains(file->fileId)) - continue; - // do not load what we already loaded in the first pass - if (userOrder.contains(file->fileId)) - continue; - file->setRemovable(true); - file->setMovable(true); - files.insert(file->order, file); - } - QSet seen; - for (auto order : files.keys()) - { - if(seen.contains(order)) - continue; - seen.insert(order); - const auto &values = files.values(order); - if(values.size() == 1) - { - profile->appendPatch(values[0]); - continue; - } - for(auto &file: values) - { - QStringList list; - for(auto &file2: values) - { - if(file != file2) - list.append(file2->name); - } - file->addProblem(PROBLEM_WARNING, QObject::tr("%1 has the same order as the following components:\n%2").arg(file->name, list.join(", "))); - profile->appendPatch(file); - } - } -} - - -void OneSixProfileStrategy::load() -{ - profile->clearPatches(); - - upgradeDeprecatedFiles(); - loadDefaultBuiltinPatches(); - loadUserPatches(); - - profile->finalize(); -} - -bool OneSixProfileStrategy::saveOrder(ProfileUtils::PatchOrder order) -{ - return ProfileUtils::writeOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), order); -} - -bool OneSixProfileStrategy::resetOrder() -{ - return QDir(m_instance->instanceRoot()).remove("order.json"); -} - -bool OneSixProfileStrategy::removePatch(ProfilePatchPtr patch) -{ - bool ok = true; - // first, remove the patch file. this ensures it's not used anymore - auto fileName = patch->getPatchFilename(); - if(fileName.size()) - { - QFile patchFile(fileName); - if(patchFile.exists() && !patchFile.remove()) - { - qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString(); - return false; - } - } - - - auto preRemoveJarMod = [&](JarmodPtr jarMod) -> bool - { - QString fullpath = FS::PathCombine(m_instance->jarModsDir(), jarMod->name); - QFileInfo finfo (fullpath); - if(finfo.exists()) - { - QFile jarModFile(fullpath); - if(!jarModFile.remove()) - { - qCritical() << "File" << fullpath << "could not be removed because:" << jarModFile.errorString(); - return false; - } - return true; - } - return true; - }; - - for(auto &jarmod: patch->getJarMods()) - { - ok &= preRemoveJarMod(jarmod); - } - return ok; -} - -bool OneSixProfileStrategy::customizePatch(ProfilePatchPtr patch) -{ - if(patch->isCustom()) - { - return false; - } - - auto filename = FS::PathCombine(m_instance->instanceRoot(), "patches" , patch->getPatchID() + ".json"); - if(!FS::ensureFilePathExists(filename)) - { - return false; - } - try - { - QSaveFile jsonFile(filename); - if(!jsonFile.open(QIODevice::WriteOnly)) - { - return false; - } - auto document = patch->toJson(true); - jsonFile.write(document.toJson()); - if(!jsonFile.commit()) - { - return false; - } - load(); - } - catch (VersionIncomplete &error) - { - qDebug() << "Version was incomplete:" << error.cause(); - } - catch (Exception &error) - { - qWarning() << "Version could not be loaded:" << error.cause(); - } - return true; -} - -bool OneSixProfileStrategy::revertPatch(ProfilePatchPtr patch) -{ - if(!patch->isCustom()) - { - // already not custom - return true; - } - auto filename = patch->getPatchFilename(); - if(!QFile::exists(filename)) - { - // already gone / not custom - return true; - } - // just kill the file and reload - bool result = QFile::remove(filename); - try - { - load(); - } - catch (VersionIncomplete &error) - { - qDebug() << "Version was incomplete:" << error.cause(); - } - catch (Exception &error) - { - qWarning() << "Version could not be loaded:" << error.cause(); - } - return result; -} - -bool OneSixProfileStrategy::installJarMods(QStringList filepaths) -{ - QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches"); - if(!FS::ensureFolderPathExists(patchDir)) - { - return false; - } - - if (!FS::ensureFolderPathExists(m_instance->jarModsDir())) - { - return false; - } - - for(auto filepath:filepaths) - { - QFileInfo sourceInfo(filepath); - auto uuid = QUuid::createUuid(); - QString id = uuid.toString().remove('{').remove('}'); - QString target_filename = id + ".jar"; - QString target_id = "org.multimc.jarmod." + id; - QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; - QString finalPath = FS::PathCombine(m_instance->jarModsDir(), target_filename); - - QFileInfo targetInfo(finalPath); - if(targetInfo.exists()) - { - return false; - } - - if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath())) - { - return false; - } - - auto f = std::make_shared(); - auto jarMod = std::make_shared(); - jarMod->name = target_filename; - jarMod->originalName = sourceInfo.completeBaseName(); - f->jarMods.append(jarMod); - f->name = target_name; - f->fileId = target_id; - f->order = profile->getFreeOrderNumber(); - QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); - f->filename = patchFileName; - f->setMovable(true); - f->setRemovable(true); - - QFile file(patchFileName); - if (!file.open(QFile::WriteOnly)) - { - qCritical() << "Error opening" << file.fileName() - << "for reading:" << file.errorString(); - return false; - } - file.write(f->toJson(true).toJson()); - file.close(); - profile->appendPatch(f); - } - profile->saveCurrentOrder(); - profile->reapplySafe(); - return true; -} - diff --git a/logic/minecraft/OneSixProfileStrategy.h b/logic/minecraft/OneSixProfileStrategy.h deleted file mode 100644 index a6abe964..00000000 --- a/logic/minecraft/OneSixProfileStrategy.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once -#include "ProfileStrategy.h" - -class OneSixInstance; - -class OneSixProfileStrategy : public ProfileStrategy -{ -public: - OneSixProfileStrategy(OneSixInstance * instance); - virtual ~OneSixProfileStrategy() {}; - virtual void load() override; - virtual bool resetOrder() override; - virtual bool saveOrder(ProfileUtils::PatchOrder order) override; - virtual bool installJarMods(QStringList filepaths) override; - virtual bool removePatch(ProfilePatchPtr patch) override; - virtual bool customizePatch(ProfilePatchPtr patch) override; - virtual bool revertPatch(ProfilePatchPtr patch) override; - -protected: - virtual void loadDefaultBuiltinPatches(); - virtual void loadUserPatches(); - void upgradeDeprecatedFiles(); - -protected: - OneSixInstance *m_instance; -}; diff --git a/logic/minecraft/OneSixUpdate.cpp b/logic/minecraft/OneSixUpdate.cpp deleted file mode 100644 index 5babb0b6..00000000 --- a/logic/minecraft/OneSixUpdate.cpp +++ /dev/null @@ -1,411 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Env.h" -#include "OneSixUpdate.h" - -#include - -#include -#include -#include -#include -#include - -#include "BaseInstance.h" -#include "minecraft/MinecraftVersionList.h" -#include "minecraft/MinecraftProfile.h" -#include "minecraft/RawLibrary.h" -#include "minecraft/OneSixInstance.h" -#include "forge/ForgeMirrors.h" -#include "net/URLConstants.h" -#include "minecraft/AssetsUtils.h" -#include "Exception.h" -#include "MMCZip.h" -#include - -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( - 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(ENV.getVersionList("net.minecraft"))->createUpdateTask(m_inst->intendedVersionId()); - if (!versionUpdateTask) - { - qDebug() << "Didn't spawn an update task."; - jarlibStart(); - return; - } - connect(versionUpdateTask.get(), SIGNAL(succeeded()), SLOT(jarlibStart())); - connect(versionUpdateTask.get(), &NetJob::failed, this, &OneSixUpdate::versionUpdateFailed); - connect(versionUpdateTask.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); - setStatus(tr("Getting the version files from Mojang...")); - versionUpdateTask->start(); -} - -void OneSixUpdate::versionUpdateFailed(QString reason) -{ - emitFailed(reason); -} - -void OneSixUpdate::assetIndexStart() -{ - setStatus(tr("Updating assets index...")); - OneSixInstance *inst = (OneSixInstance *)m_inst; - std::shared_ptr 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); - entry->stale = true; - job->addNetAction(CacheDownload::make(indexUrl, entry)); - jarlibDownloadJob.reset(job); - - connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetIndexFinished())); - connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::assetIndexFailed); - connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); - - qDebug() << m_inst->name() << ": Starting asset index download"; - jarlibDownloadJob->start(); -} - -void OneSixUpdate::assetIndexFinished() -{ - AssetsIndex index; - qDebug() << m_inst->name() << ": Finished asset index download"; - - OneSixInstance *inst = (OneSixInstance *)m_inst; - std::shared_ptr 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 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(), &NetJob::failed, this, &OneSixUpdate::assetsFailed); - connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); - jarlibDownloadJob->start(); - return; - } - assetsFinished(); -} - -void OneSixUpdate::assetIndexFailed(QString reason) -{ - qDebug() << m_inst->name() << ": Failed asset index download"; - emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); -} - -void OneSixUpdate::assetsFinished() -{ - emitSucceeded(); -} - -void OneSixUpdate::assetsFailed(QString reason) -{ - emitFailed(tr("Failed to download assets:\n%1").arg(reason)); -} - -void OneSixUpdate::jarlibStart() -{ - setStatus(tr("Getting the library files from Mojang...")); - qDebug() << m_inst->name() << ": downloading libraries"; - OneSixInstance *inst = (OneSixInstance *)m_inst; - try - { - inst->reloadProfile(); - } - catch (Exception &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 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 ForgeLibs; - QList brokenLocalLibs; - - for (auto lib : libs) - { - if (lib->hint() == "local") - { - if (!lib->filesExist(m_inst->librariesPath())) - brokenLocalLibs.append(lib); - continue; - } - - QString raw_storage = lib->storageSuffix(); - QString raw_dl = lib->url(); - - 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(), &NetJob::failed, this, &OneSixUpdate::jarlibFailed); - connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), - SIGNAL(progress(qint64, qint64))); - - jarlibDownloadJob->start(); -} - -void OneSixUpdate::jarlibFinished() -{ - OneSixInstance *inst = (OneSixInstance *)m_inst; - std::shared_ptr version = inst->getMinecraftProfile(); - - if (version->traits.contains("legacyFML")) - { - fmllibsStart(); - } - else - { - assetIndexStart(); - } -} - -void OneSixUpdate::jarlibFailed(QString reason) -{ - QStringList failed = jarlibDownloadJob->getFailedFiles(); - QString failed_all = failed.join("\n"); - emitFailed( - tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); -} - -void OneSixUpdate::fmllibsStart() -{ - // Get the mod list - OneSixInstance *inst = (OneSixInstance *)m_inst; - std::shared_ptr 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(FS::PathCombine(inst->libDir(), lib.filename)); - if (libInfo.exists()) - continue; - fmlLibsToProcess.append(lib); - } - - // if everything is in place, there's nothing to do here... - if (fmlLibsToProcess.isEmpty()) - { - assetIndexStart(); - return; - } - - // download missing libs to our place - setStatus(tr("Dowloading FML libraries...")); - auto dljob = new NetJob("FML libraries"); - auto metacache = ENV.metacache(); - for (auto &lib : fmlLibsToProcess) - { - auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename - : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; - dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry)); - } - - connect(dljob, SIGNAL(succeeded()), SLOT(fmllibsFinished())); - connect(dljob, &NetJob::failed, this, &OneSixUpdate::fmllibsFailed); - connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); - legacyDownloadJob.reset(dljob); - legacyDownloadJob->start(); -} - -void OneSixUpdate::fmllibsFinished() -{ - legacyDownloadJob.reset(); - if (!fmlLibsToProcess.isEmpty()) - { - setStatus(tr("Copying FML libraries into the instance...")); - OneSixInstance *inst = (OneSixInstance *)m_inst; - auto metacache = ENV.metacache(); - int index = 0; - for (auto &lib : fmlLibsToProcess) - { - progress(index, fmlLibsToProcess.size()); - auto entry = metacache->resolveEntry("fmllibs", lib.filename); - auto path = FS::PathCombine(inst->libDir(), lib.filename); - if (!FS::ensureFilePathExists(path)) - { - emitFailed(tr("Failed creating FML library folder inside the instance.")); - return; - } - if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) - { - emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); - return; - } - index++; - } - progress(index, fmlLibsToProcess.size()); - } - assetIndexStart(); -} - -void OneSixUpdate::fmllibsFailed(QString reason) -{ - emitFailed(tr("Game update failed: it was impossible to fetch the required FML libraries.\nReason:\n%1").arg(reason)); - return; -} - diff --git a/logic/minecraft/OneSixUpdate.h b/logic/minecraft/OneSixUpdate.h deleted file mode 100644 index 6901e3d6..00000000 --- a/logic/minecraft/OneSixUpdate.h +++ /dev/null @@ -1,68 +0,0 @@ -/* 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 -#include -#include - -#include "net/NetJob.h" -#include "tasks/Task.h" -#include "minecraft/VersionFilterData.h" -#include - -class MinecraftVersion; -class OneSixInstance; - -class OneSixUpdate : public Task -{ - Q_OBJECT -public: - explicit OneSixUpdate(OneSixInstance *inst, QObject *parent = 0); - virtual void executeTask(); - -private -slots: - void versionUpdateFailed(QString reason); - - void jarlibStart(); - void jarlibFinished(); - void jarlibFailed(QString reason); - - void fmllibsStart(); - void fmllibsFinished(); - void fmllibsFailed(QString reason); - - void assetIndexStart(); - void assetIndexFinished(); - void assetIndexFailed(QString reason); - - void assetsFinished(); - void assetsFailed(QString reason); - -private: - NetJobPtr jarlibDownloadJob; - NetJobPtr legacyDownloadJob; - - /// target version, determined during this task - std::shared_ptr targetVersion; - /// the task that is spawned for version updates - std::shared_ptr versionUpdateTask; - - OneSixInstance *m_inst = nullptr; - QString jarHashOnEntry; - QList fmlLibsToProcess; -}; diff --git a/logic/minecraft/RawLibrary.h b/logic/minecraft/RawLibrary.h index 7f0aed2f..62791239 100644 --- a/logic/minecraft/RawLibrary.h +++ b/logic/minecraft/RawLibrary.h @@ -8,7 +8,7 @@ #include #include -#include "minecraft/OneSixRule.h" +#include "OneSixRule.h" #include "minecraft/OpSys.h" #include "GradleSpecifier.h" #include "net/URLConstants.h" diff --git a/logic/minecraft/auth/AuthSession.cpp b/logic/minecraft/auth/AuthSession.cpp new file mode 100644 index 00000000..8758bfbd --- /dev/null +++ b/logic/minecraft/auth/AuthSession.cpp @@ -0,0 +1,30 @@ +#include "AuthSession.h" +#include +#include +#include +#include + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); + +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + if (status != PlayableOffline && status != PlayableOnline) + { + return false; + } + session = "-"; + player_name = offline_playername; + status = PlayableOffline; + return true; +} diff --git a/logic/minecraft/auth/AuthSession.h b/logic/minecraft/auth/AuthSession.h new file mode 100644 index 00000000..dede90a9 --- /dev/null +++ b/logic/minecraft/auth/AuthSession.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +#include "multimc_logic_export.h" + +struct User +{ + QString id; + QMultiMap properties; +}; + +struct MULTIMC_LOGIC_EXPORT AuthSession +{ + bool MakeOffline(QString offline_playername); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresPassword, + PlayableOffline, + PlayableOnline + } status = Undetermined; + + User u; + + // client token + QString client_token; + // account user name + QString username; + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; +}; + +typedef std::shared_ptr AuthSessionPtr; diff --git a/logic/minecraft/auth/MojangAccount.cpp b/logic/minecraft/auth/MojangAccount.cpp new file mode 100644 index 00000000..69a24c09 --- /dev/null +++ b/logic/minecraft/auth/MojangAccount.cpp @@ -0,0 +1,278 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MojangAccount.h" +#include "flows/RefreshTask.h" +#include "flows/AuthenticateTask.h" + +#include +#include +#include +#include +#include +#include + +#include + +MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) +{ + // The JSON object must at least have a username for it to be valid. + if (!object.value("username").isString()) + { + qCritical() << "Can't load Mojang account info from JSON object. Username field is " + "missing or of the wrong type."; + return nullptr; + } + + QString username = object.value("username").toString(""); + QString clientToken = object.value("clientToken").toString(""); + QString accessToken = object.value("accessToken").toString(""); + + QJsonArray profileArray = object.value("profiles").toArray(); + if (profileArray.size() < 1) + { + qCritical() << "Can't load Mojang account with username \"" << username + << "\". No profiles found."; + return nullptr; + } + + QList profiles; + for (QJsonValue profileVal : profileArray) + { + QJsonObject profileObject = profileVal.toObject(); + QString id = profileObject.value("id").toString(""); + QString name = profileObject.value("name").toString(""); + bool legacy = profileObject.value("legacy").toBool(false); + if (id.isEmpty() || name.isEmpty()) + { + qWarning() << "Unable to load a profile because it was missing an ID or a name."; + continue; + } + profiles.append({id, name, legacy}); + } + + MojangAccountPtr account(new MojangAccount()); + if (object.value("user").isObject()) + { + User u; + QJsonObject userStructure = object.value("user").toObject(); + u.id = userStructure.value("id").toString(); + /* + QJsonObject propMap = userStructure.value("properties").toObject(); + for(auto key: propMap.keys()) + { + auto values = propMap.operator[](key).toArray(); + for(auto value: values) + u.properties.insert(key, value.toString()); + } + */ + account->m_user = u; + } + account->m_username = username; + account->m_clientToken = clientToken; + account->m_accessToken = accessToken; + account->m_profiles = profiles; + + // Get the currently selected profile. + QString currentProfile = object.value("activeProfile").toString(""); + if (!currentProfile.isEmpty()) + account->setCurrentProfile(currentProfile); + + return account; +} + +MojangAccountPtr MojangAccount::createFromUsername(const QString &username) +{ + MojangAccountPtr account(new MojangAccount()); + account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->m_username = username; + return account; +} + +QJsonObject MojangAccount::saveToJson() const +{ + QJsonObject json; + json.insert("username", m_username); + json.insert("clientToken", m_clientToken); + json.insert("accessToken", m_accessToken); + + QJsonArray profileArray; + for (AccountProfile profile : m_profiles) + { + QJsonObject profileObj; + profileObj.insert("id", profile.id); + profileObj.insert("name", profile.name); + profileObj.insert("legacy", profile.legacy); + profileArray.append(profileObj); + } + json.insert("profiles", profileArray); + + QJsonObject userStructure; + { + userStructure.insert("id", m_user.id); + /* + QJsonObject userAttrs; + for(auto key: m_user.properties.keys()) + { + auto array = QJsonArray::fromStringList(m_user.properties.values(key)); + userAttrs.insert(key, array); + } + userStructure.insert("properties", userAttrs); + */ + } + json.insert("user", userStructure); + + if (m_currentProfile != -1) + json.insert("activeProfile", currentProfile()->id); + + return json; +} + +bool MojangAccount::setCurrentProfile(const QString &profileId) +{ + for (int i = 0; i < m_profiles.length(); i++) + { + if (m_profiles[i].id == profileId) + { + m_currentProfile = i; + return true; + } + } + return false; +} + +const AccountProfile *MojangAccount::currentProfile() const +{ + if (m_currentProfile == -1) + return nullptr; + return &m_profiles[m_currentProfile]; +} + +AccountStatus MojangAccount::accountStatus() const +{ + if (m_accessToken.isEmpty()) + return NotVerified; + else + return Verified; +} + +std::shared_ptr MojangAccount::login(AuthSessionPtr session, + QString password) +{ + Q_ASSERT(m_currentTask.get() == nullptr); + + // take care of the true offline status + if (accountStatus() == NotVerified && password.isEmpty()) + { + if (session) + { + session->status = AuthSession::RequiresPassword; + fillSession(session); + } + return nullptr; + } + + if (password.isEmpty()) + { + m_currentTask.reset(new RefreshTask(this)); + } + else + { + m_currentTask.reset(new AuthenticateTask(this, password)); + } + m_currentTask->assignSession(session); + + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + return m_currentTask; +} + +void MojangAccount::authSucceeded() +{ + auto session = m_currentTask->getAssignedSession(); + if (session) + { + session->status = + session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline; + fillSession(session); + session->auth_server_online = true; + } + m_currentTask.reset(); + emit changed(); +} + +void MojangAccount::authFailed(QString reason) +{ + auto session = m_currentTask->getAssignedSession(); + // This is emitted when the yggdrasil tasks time out or are cancelled. + // -> we treat the error as no-op + if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT) + { + if (session) + { + session->status = accountStatus() == Verified ? AuthSession::PlayableOffline + : AuthSession::RequiresPassword; + session->auth_server_online = false; + fillSession(session); + } + } + else + { + m_accessToken = QString(); + emit changed(); + if (session) + { + session->status = AuthSession::RequiresPassword; + session->auth_server_online = true; + fillSession(session); + } + } + m_currentTask.reset(); +} + +void MojangAccount::fillSession(AuthSessionPtr session) +{ + // the user name. you have to have an user name + session->username = m_username; + // volatile auth token + session->access_token = m_accessToken; + // the semi-permanent client token + session->client_token = m_clientToken; + if (currentProfile()) + { + // profile name + session->player_name = currentProfile()->name; + // profile ID + session->uuid = currentProfile()->id; + // 'legacy' or 'mojang', depending on account type + session->user_type = currentProfile()->legacy ? "legacy" : "mojang"; + if (!session->access_token.isEmpty()) + { + session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; + } + else + { + session->session = "-"; + } + } + else + { + session->player_name = "Player"; + session->session = "-"; + } + session->u = user(); +} diff --git a/logic/minecraft/auth/MojangAccount.h b/logic/minecraft/auth/MojangAccount.h new file mode 100644 index 00000000..2de0c19c --- /dev/null +++ b/logic/minecraft/auth/MojangAccount.h @@ -0,0 +1,173 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include "AuthSession.h" + +#include "multimc_logic_export.h" + +class Task; +class YggdrasilTask; +class MojangAccount; + +typedef std::shared_ptr MojangAccountPtr; +Q_DECLARE_METATYPE(MojangAccountPtr) + +/** + * A profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in MultiMC right now so + * we don't have to rip the code to pieces to add it later. + */ +struct AccountProfile +{ + QString id; + QString name; + bool legacy; +}; + +enum AccountStatus +{ + NotVerified, + Verified +}; + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MULTIMC_LOGIC_EXPORT MojangAccount : public QObject +{ + Q_OBJECT +public: /* construction */ + //! Do not copy accounts. ever. + explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; + + //! Default constructor + explicit MojangAccount(QObject *parent = 0) : QObject(parent) {}; + + //! Creates an empty account for the specified user name. + static MojangAccountPtr createFromUsername(const QString &username); + + //! Loads a MojangAccount from the given JSON object. + static MojangAccountPtr loadFromJson(const QJsonObject &json); + + //! Saves a MojangAccount to a JSON object and returns it. + QJsonObject saveToJson() const; + +public: /* manipulation */ + /** + * Sets the currently selected profile to the profile with the given ID string. + * If profileId is not in the list of available profiles, the function will simply return + * false. + */ + bool setCurrentProfile(const QString &profileId); + + /** + * Attempt to login. Empty password means we use the token. + * If the attempt fails because we already are performing some task, it returns false. + */ + std::shared_ptr login(AuthSessionPtr session, + QString password = QString()); + +public: /* queries */ + const QString &username() const + { + return m_username; + } + + const QString &clientToken() const + { + return m_clientToken; + } + + const QString &accessToken() const + { + return m_accessToken; + } + + const QList &profiles() const + { + return m_profiles; + } + + const User &user() + { + return m_user; + } + + //! Returns the currently selected profile (if none, returns nullptr) + const AccountProfile *currentProfile() const; + + //! Returns whether the account is NotVerified, Verified or Online + AccountStatus accountStatus() const; + +signals: + /** + * This signal is emitted when the account changes + */ + void changed(); + + // TODO: better signalling for the various possible state changes - especially errors + +protected: /* variables */ + QString m_username; + + // Used to identify the client - the user can have multiple clients for the same account + // Think: different launchers, all connecting to the same account/profile + QString m_clientToken; + + // Blank if not logged in. + QString m_accessToken; + + // Index of the selected profile within the list of available + // profiles. -1 if nothing is selected. + int m_currentProfile = -1; + + // List of available profiles. + QList m_profiles; + + // the user structure, whatever it is. + User m_user; + + // current task we are executing here + std::shared_ptr m_currentTask; + +private +slots: + void authSucceeded(); + void authFailed(QString reason); + +private: + void fillSession(AuthSessionPtr session); + +public: + friend class YggdrasilTask; + friend class AuthenticateTask; + friend class ValidateTask; + friend class RefreshTask; +}; diff --git a/logic/minecraft/auth/MojangAccountList.cpp b/logic/minecraft/auth/MojangAccountList.cpp new file mode 100644 index 00000000..26cbc81a --- /dev/null +++ b/logic/minecraft/auth/MojangAccountList.cpp @@ -0,0 +1,427 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MojangAccountList.h" +#include "MojangAccount.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#define ACCOUNT_LIST_FORMAT_VERSION 2 + +MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) +{ +} + +MojangAccountPtr MojangAccountList::findAccount(const QString &username) const +{ + for (int i = 0; i < count(); i++) + { + MojangAccountPtr account = at(i); + if (account->username() == username) + return account; + } + return nullptr; +} + +const MojangAccountPtr MojangAccountList::at(int i) const +{ + return MojangAccountPtr(m_accounts.at(i)); +} + +void MojangAccountList::addAccount(const MojangAccountPtr account) +{ + beginResetModel(); + connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + m_accounts.append(account); + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(const QString &username) +{ + beginResetModel(); + for (auto account : m_accounts) + { + if (account->username() == username) + { + m_accounts.removeOne(account); + return; + } + } + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(QModelIndex index) +{ + beginResetModel(); + m_accounts.removeAt(index.row()); + endResetModel(); + onListChanged(); +} + +MojangAccountPtr MojangAccountList::activeAccount() const +{ + return m_activeAccount; +} + +void MojangAccountList::setActiveAccount(const QString &username) +{ + beginResetModel(); + if (username.isEmpty()) + { + m_activeAccount = nullptr; + } + else + { + for (MojangAccountPtr account : m_accounts) + { + if (account->username() == username) + m_activeAccount = account; + } + } + endResetModel(); + onActiveChanged(); +} + +void MojangAccountList::accountChanged() +{ + // the list changed. there is no doubt. + onListChanged(); +} + +void MojangAccountList::onListChanged() +{ + if (m_autosave) + // TODO: Alert the user if this fails. + saveList(); + + emit listChanged(); +} + +void MojangAccountList::onActiveChanged() +{ + if (m_autosave) + saveList(); + + emit activeAccountChanged(); +} + +int MojangAccountList::count() const +{ + return m_accounts.count(); +} + +QVariant MojangAccountList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MojangAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return account->username(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return account->username(); + + case PointerRole: + return qVariantFromValue(account); + + case Qt::CheckStateRole: + switch (index.column()) + { + case ActiveColumn: + return account == m_activeAccount; + } + + default: + return QVariant(); + } +} + +QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return tr("Active?"); + + case NameColumn: + return tr("Name"); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("The name of the version."); + + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +int MojangAccountList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int MojangAccountList::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if(role == Qt::CheckStateRole) + { + if(value == Qt::Checked) + { + MojangAccountPtr account = this->at(index.row()); + this->setActiveAccount(account->username()); + } + } + + emit dataChanged(index, index); + return true; +} + +void MojangAccountList::updateListData(QList versions) +{ + beginResetModel(); + m_accounts = versions; + endResetModel(); +} + +bool MojangAccountList::loadList(const QString &filePath) +{ + QString path = filePath; + if (path.isEmpty()) + path = m_listFilePath; + if (path.isEmpty()) + { + qCritical() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION) + { + QString newName = "accounts-old.json"; + qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to" + << newName; + + // Attempt to rename the old version. + file.rename(newName); + return false; + } + + // Now, load the accounts array. + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MojangAccountPtr account = MojangAccount::loadFromJson(accountObj); + if (account.get() != nullptr) + { + connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + m_accounts.append(account); + } + else + { + qWarning() << "Failed to load an account."; + } + } + // Load the active account. + m_activeAccount = findAccount(root.value("activeAccount").toString("")); + endResetModel(); + return true; +} + +bool MojangAccountList::saveList(const QString &filePath) +{ + QString path(filePath); + if (path.isEmpty()) + path = m_listFilePath; + if (path.isEmpty()) + { + qCritical() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + // make sure the parent folder exists + if(!FS::ensureFilePathExists(path)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(path); + if(finfo.isDir()) + { + QDir badDir(path); + badDir.removeRecursively(); + } + + qDebug() << "Writing account list to" << path; + + qDebug() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION); + + // Build a list of accounts. + qDebug() << "Building account array."; + QJsonArray accounts; + for (MojangAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + if(m_activeAccount) + { + // Save the active account. + root.insert("activeAccount", m_activeAccount->username()); + } + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + // Now that we're done building the JSON object, we can write it to the file. + qDebug() << "Writing account list to file."; + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser); + file.close(); + + qDebug() << "Saved account list to" << path; + + return true; +} + +void MojangAccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + m_autosave = autosave; +} + +bool MojangAccountList::anyAccountIsValid() +{ + for(auto account:m_accounts) + { + if(account->accountStatus() != NotVerified) + return true; + } + return false; +} diff --git a/logic/minecraft/auth/MojangAccountList.h b/logic/minecraft/auth/MojangAccountList.h new file mode 100644 index 00000000..c40fa6a3 --- /dev/null +++ b/logic/minecraft/auth/MojangAccountList.h @@ -0,0 +1,201 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "MojangAccount.h" + +#include +#include +#include +#include + +#include "multimc_logic_export.h" + +/*! + * \brief List of available Mojang accounts. + * This should be loaded in the background by MultiMC on startup. + * + * This class also inherits from QAbstractListModel. Methods from that + * class determine how this list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by subclasses to + * change the behavior of the list. + */ +class MULTIMC_LOGIC_EXPORT MojangAccountList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // TODO: Add icon column. + + // First column - Active? + ActiveColumn = 0, + + // Second column - Name + NameColumn, + }; + + explicit MojangAccountList(QObject *parent = 0); + + //! Gets the account at the given index. + virtual const MojangAccountPtr at(int i) const; + + //! Returns the number of accounts in the list. + virtual int count() const; + + //////// List Model Functions //////// + virtual QVariant data(const QModelIndex &index, int role) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + virtual int rowCount(const QModelIndex &parent) const; + virtual int columnCount(const QModelIndex &parent) const; + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role); + + /*! + * Adds a the given Mojang account to the account list. + */ + virtual void addAccount(const MojangAccountPtr account); + + /*! + * Removes the mojang account with the given username from the account list. + */ + virtual void removeAccount(const QString &username); + + /*! + * Removes the account at the given QModelIndex. + */ + virtual void removeAccount(QModelIndex index); + + /*! + * \brief Finds an account by its username. + * \param The username of the account to find. + * \return A const pointer to the account with the given username. NULL if + * one doesn't exist. + */ + virtual MojangAccountPtr findAccount(const QString &username) const; + + /*! + * Sets the default path to save the list file to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + virtual void setListFilePath(QString path, bool autosave = false); + + /*! + * \brief Loads the account list from the given file path. + * If the given file is an empty string (default), will load from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool loadList(const QString &file = ""); + + /*! + * \brief Saves the account list to the given file. + * If the given file is an empty string (default), will save from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool saveList(const QString &file = ""); + + /*! + * \brief Gets a pointer to the account that the user has selected as their "active" account. + * Which account is active can be overridden on a per-instance basis, but this will return the one that + * is set as active globally. + * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer. + */ + virtual MojangAccountPtr activeAccount() const; + + /*! + * Sets the given account as the current active account. + * If the username given is an empty string, sets the active account to nothing. + */ + virtual void setActiveAccount(const QString &username); + + /*! + * Returns true if any of the account is at least Validated + */ + bool anyAccountIsValid(); + +signals: + /*! + * Signal emitted to indicate that the account list has changed. + * This will also fire if the value of an element in the list changes (will be implemented + * later). + */ + void listChanged(); + + /*! + * Signal emitted to indicate that the active account has changed. + */ + void activeAccountChanged(); + +public +slots: + /** + * This is called when one of the accounts changes and the list needs to be updated + */ + void accountChanged(); + +protected: + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + /*! + * Called whenever the active account changes. + * Emits the activeAccountChanged() signal and autosaves the list if enabled. + */ + void onActiveChanged(); + + QList m_accounts; + + /*! + * Account that is currently active. + */ + MojangAccountPtr m_activeAccount; + + //! Path to the account list file. Empty string if there isn't one. + QString m_listFilePath; + + /*! + * If true, the account list will automatically save to the account list path when it changes. + * Ignored if m_listFilePath is blank. + */ + bool m_autosave = false; + +protected +slots: + /*! + * Updates this list with the given list of accounts. + * This is done by copying each account in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the accounts are set to this + * account list. This can't be done in the load task, because the accounts the load + * task creates are on the load task's thread and Qt won't allow their parents + * to be set to something created on another thread. + * To get around that problem, we invoke this method on the GUI thread, which + * then copies the accounts and sets their parents correctly. + * \param accounts List of accounts whose parents should be set. + */ + virtual void updateListData(QList versions); +}; diff --git a/logic/minecraft/auth/YggdrasilTask.cpp b/logic/minecraft/auth/YggdrasilTask.cpp new file mode 100644 index 00000000..c6971c9f --- /dev/null +++ b/logic/minecraft/auth/YggdrasilTask.cpp @@ -0,0 +1,255 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "YggdrasilTask.h" +#include "MojangAccount.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent) + : Task(parent), m_account(account) +{ + changeState(STATE_CREATED); +} + +void YggdrasilTask::executeTask() +{ + changeState(STATE_SENDING_REQUEST); + + // Get the content of the request we're going to send to the server. + QJsonDocument doc(getRequestContent()); + + auto worker = ENV.qnam(); + QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint()); + QNetworkRequest netRequest(reqUrl); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QByteArray requestData = doc.toJson(); + m_netReply = worker->post(netRequest, requestData); + connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors); + timeout_keeper.setSingleShot(true); + timeout_keeper.start(timeout_max); + counter.setSingleShot(false); + counter.start(time_step); + progress(0, timeout_max); + connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout); + connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); +} + +void YggdrasilTask::refreshTimers(qint64, qint64) +{ + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} +void YggdrasilTask::heartbeat() +{ + count += time_step; + progress(count, timeout_max); +} + +bool YggdrasilTask::abort() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = YggdrasilTask::BY_USER; + m_netReply->abort(); + return true; +} + +void YggdrasilTask::abortByTimeout() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = YggdrasilTask::BY_TIMEOUT; + m_netReply->abort(); +} + +void YggdrasilTask::sslErrors(QList errors) +{ + int i = 1; + for (auto error : errors) + { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +void YggdrasilTask::processReply() +{ + changeState(STATE_PROCESSING_RESPONSE); + + switch (m_netReply->error()) + { + case QNetworkReply::NoError: + break; + case QNetworkReply::TimeoutError: + changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out.")); + return; + case QNetworkReply::OperationCanceledError: + changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); + return; + case QNetworkReply::SslHandshakeFailedError: + changeState( + STATE_FAILED_SOFT, + tr("SSL Handshake failed.
There might be a few causes for it:
" + "
    " + "
  • You use Windows XP and need to update " + "your root certificates
  • " + "
  • Some device on your network is interfering with SSL traffic. In that case, " + "you have bigger worries than Minecraft not starting.
  • " + "
  • Possibly something else. Check the MultiMC log file for details
  • " + "
")); + return; + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::ContentOperationNotPermittedError: + break; + default: + changeState(STATE_FAILED_SOFT, + tr("Authentication operation failed due to a network error: %1 (%2)") + .arg(m_netReply->errorString()).arg(m_netReply->error())); + return; + } + + // Try to parse the response regardless of the response code. + // Sometimes the auth server will give more information and an error code. + QJsonParseError jsonError; + QByteArray replyData = m_netReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + // Check the response code. + int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) + { + // If the response code was 200, then there shouldn't be an error. Make sure + // anyways. + // Also, sometimes an empty reply indicates success. If there was no data received, + // pass an empty json object to the processResponse function. + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) + { + processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); + return; + } + else + { + changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response " + "JSON response: %1 at offset %2.") + .arg(jsonError.errorString()) + .arg(jsonError.offset)); + qCritical() << replyData; + } + return; + } + + // If the response code was not 200, then Yggdrasil may have given us information + // about the error. + // If we can parse the response, then get information from it. Otherwise just say + // there was an unknown error. + if (jsonError.error == QJsonParseError::NoError) + { + // We were able to parse the server's response. Woo! + // Call processError. If a subclass has overridden it then they'll handle their + // stuff there. + qDebug() << "The request failed, but the server gave us an error message. " + "Processing error."; + processError(doc.object()); + } + else + { + // The server didn't say anything regarding the error. Give the user an unknown + // error. + qDebug() + << "The request failed and the server gave no error message. Unknown error."; + changeState(STATE_FAILED_SOFT, + tr("An unknown error occurred when trying to communicate with the " + "authentication server: %1").arg(m_netReply->errorString())); + } +} + +void YggdrasilTask::processError(QJsonObject responseData) +{ + QJsonValue errorVal = responseData.value("error"); + QJsonValue errorMessageValue = responseData.value("errorMessage"); + QJsonValue causeVal = responseData.value("cause"); + + if (errorVal.isString() && errorMessageValue.isString()) + { + m_error = std::shared_ptr(new Error{ + errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")}); + changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); + } + else + { + // Error is not in standard format. Don't set m_error and return unknown error. + changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); + } +} + +QString YggdrasilTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_CREATED: + return "Waiting..."; + case STATE_SENDING_REQUEST: + return tr("Sending request to auth servers..."); + case STATE_PROCESSING_RESPONSE: + return tr("Processing response from servers..."); + case STATE_SUCCEEDED: + return tr("Authentication task succeeded."); + case STATE_FAILED_SOFT: + return tr("Failed to contact the authentication server."); + case STATE_FAILED_HARD: + return tr("Failed to authenticate."); + default: + return tr("..."); + } +} + +void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason) +{ + m_state = newState; + setStatus(getStateMessage()); + if (newState == STATE_SUCCEEDED) + { + emitSucceeded(); + } + else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT) + { + emitFailed(reason); + } +} + +YggdrasilTask::State YggdrasilTask::state() +{ + return m_state; +} diff --git a/logic/minecraft/auth/YggdrasilTask.h b/logic/minecraft/auth/YggdrasilTask.h new file mode 100644 index 00000000..c84cfc06 --- /dev/null +++ b/logic/minecraft/auth/YggdrasilTask.h @@ -0,0 +1,150 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "MojangAccount.h" + +class QNetworkReply; + +/** + * A Yggdrasil task is a task that performs an operation on a given mojang account. + */ +class YggdrasilTask : public Task +{ + Q_OBJECT +public: + explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); + + /** + * assign a session to this task. the session will be filled with required infomration + * upon completion + */ + void assignSession(AuthSessionPtr session) + { + m_session = session; + } + + /// get the assigned session for filling with information. + AuthSessionPtr getAssignedSession() + { + return m_session; + } + + /** + * Class describing a Yggdrasil error response. + */ + struct Error + { + QString m_errorMessageShort; + QString m_errorMessageVerbose; + QString m_cause; + }; + + enum AbortedBy + { + BY_NOTHING, + BY_USER, + BY_TIMEOUT + } m_aborted = BY_NOTHING; + + /** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ + enum State + { + STATE_CREATED, + STATE_SENDING_REQUEST, + STATE_PROCESSING_RESPONSE, + STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated + STATE_FAILED_HARD, //!< hard failure. auth is invalid + STATE_SUCCEEDED + } m_state = STATE_CREATED; + +protected: + + virtual void executeTask() override; + + /** + * Gets the JSON object that will be sent to the authentication server. + * Should be overridden by subclasses. + */ + virtual QJsonObject getRequestContent() const = 0; + + /** + * Gets the endpoint to POST to. + * No leading slash. + */ + virtual QString getEndpoint() const = 0; + + /** + * Processes the response received from the server. + * If an error occurred, this should emit a failed signal and return false. + * If Yggdrasil gave an error response, it should call setError() first, and then return false. + * Otherwise, it should return true. + * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with + * an empty QJsonObject. + */ + virtual void processResponse(QJsonObject responseData) = 0; + + /** + * Processes an error response received from the server. + * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. + * \returns a QString error message that will be passed to emitFailed. + */ + virtual void processError(QJsonObject responseData); + + /** + * Returns the state message for the given state. + * Used to set the status message for the task. + * Should be overridden by subclasses that want to change messages for a given state. + */ + virtual QString getStateMessage() const; + +protected +slots: + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + void sslErrors(QList); + + void changeState(State newState, QString reason=QString()); +public +slots: + virtual bool abort() override; + void abortByTimeout(); + State state(); +protected: + // FIXME: segfault disaster waiting to happen + MojangAccount *m_account = nullptr; + QNetworkReply *m_netReply = nullptr; + std::shared_ptr m_error; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 30000; + const int time_step = 50; + + AuthSessionPtr m_session; +}; diff --git a/logic/minecraft/auth/flows/AuthenticateTask.cpp b/logic/minecraft/auth/flows/AuthenticateTask.cpp new file mode 100644 index 00000000..8d136f0b --- /dev/null +++ b/logic/minecraft/auth/flows/AuthenticateTask.cpp @@ -0,0 +1,202 @@ + +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AuthenticateTask.h" +#include "../MojangAccount.h" + +#include +#include +#include +#include + +#include +#include + +AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password, + QObject *parent) + : YggdrasilTask(account, parent), m_password(password) +{ +} + +QJsonObject AuthenticateTask::getRequestContent() const +{ + /* + * { + * "agent": { // optional + * "name": "Minecraft", // So far this is the only encountered value + * "version": 1 // This number might be increased + * // by the vanilla client in the future + * }, + * "username": "mojang account name", // Can be an email address or player name for + // unmigrated accounts + * "password": "mojang account password", + * "clientToken": "client identifier" // optional + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + + { + QJsonObject agent; + // C++ makes string literals void* for some stupid reason, so we have to tell it + // QString... Thanks Obama. + agent.insert("name", QString("Minecraft")); + agent.insert("version", 1); + req.insert("agent", agent); + } + + req.insert("username", m_account->username()); + req.insert("password", m_password); + req.insert("requestUser", true); + + // If we already have a client token, give it to the server. + // Otherwise, let the server give us one. + + if(m_account->m_clientToken.isEmpty()) + { + auto uuid = QUuid::createUuid(); + auto uuidString = uuid.toString().remove('{').remove('-').remove('}'); + m_account->m_clientToken = uuidString; + } + req.insert("clientToken", m_account->m_clientToken); + + return req; +} + +void AuthenticateTask::processResponse(QJsonObject responseData) +{ + // Read the response data. We need to get the client token, access token, and the selected + // profile. + qDebug() << "Processing authentication response."; + // qDebug() << responseData; + // If we already have a client token, make sure the one the server gave us matches our + // existing one. + qDebug() << "Getting client token."; + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) + { + // Fail if the server gave us an empty client token + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + return; + } + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) + { + changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + return; + } + // Set the client token. + m_account->m_clientToken = clientToken; + + // Now, we set the access token. + qDebug() << "Getting access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) + { + // Fail if the server didn't give us an access token. + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + return; + } + // Set the access token. + m_account->m_accessToken = accessToken; + + // Now we load the list of available profiles. + // Mojang hasn't yet implemented the profile system, + // but we might as well support what's there so we + // don't have trouble implementing it later. + qDebug() << "Loading profile list."; + QJsonArray availableProfiles = responseData.value("availableProfiles").toArray(); + QList loadedProfiles; + for (auto iter : availableProfiles) + { + QJsonObject profile = iter.toObject(); + // Profiles are easy, we just need their ID and name. + QString id = profile.value("id").toString(""); + QString name = profile.value("name").toString(""); + bool legacy = profile.value("legacy").toBool(false); + + if (id.isEmpty() || name.isEmpty()) + { + // This should never happen, but we might as well + // warn about it if it does so we can debug it easily. + // You never know when Mojang might do something truly derpy. + qWarning() << "Found entry in available profiles list with missing ID or name " + "field. Ignoring it."; + } + + // Now, add a new AccountProfile entry to the list. + loadedProfiles.append({id, name, legacy}); + } + // Put the list of profiles we loaded into the MojangAccount object. + m_account->m_profiles = loadedProfiles; + + // Finally, we set the current profile to the correct value. This is pretty simple. + // We do need to make sure that the current profile that the server gave us + // is actually in the available profiles list. + // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know). + qDebug() << "Setting current profile."; + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (currentProfileId.isEmpty()) + { + changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium.")); + return; + } + if (!m_account->setCurrentProfile(currentProfileId)) + { + changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list.")); + return; + } + + // this is what the vanilla launcher passes to the userProperties launch param + if (responseData.contains("user")) + { + User u; + auto obj = responseData.value("user").toObject(); + u.id = obj.value("id").toString(); + auto propArray = obj.value("properties").toArray(); + for (auto prop : propArray) + { + auto propTuple = prop.toObject(); + auto name = propTuple.value("name").toString(); + auto value = propTuple.value("value").toString(); + u.properties.insert(name, value); + } + m_account->m_user = u; + } + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading authentication response."; + changeState(STATE_SUCCEEDED); +} + +QString AuthenticateTask::getEndpoint() const +{ + return "authenticate"; +} + +QString AuthenticateTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_SENDING_REQUEST: + return tr("Authenticating: Sending request..."); + case STATE_PROCESSING_RESPONSE: + return tr("Authenticating: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/logic/minecraft/auth/flows/AuthenticateTask.h b/logic/minecraft/auth/flows/AuthenticateTask.h new file mode 100644 index 00000000..398fab98 --- /dev/null +++ b/logic/minecraft/auth/flows/AuthenticateTask.h @@ -0,0 +1,46 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include +#include +#include + +/** + * The authenticate task takes a MojangAccount with no access token and password and attempts to + * authenticate with Mojang's servers. + * If successful, it will set the MojangAccount's access token. + */ +class AuthenticateTask : public YggdrasilTask +{ + Q_OBJECT +public: + AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; + +private: + QString m_password; +}; diff --git a/logic/minecraft/auth/flows/RefreshTask.cpp b/logic/minecraft/auth/flows/RefreshTask.cpp new file mode 100644 index 00000000..a0fb2e48 --- /dev/null +++ b/logic/minecraft/auth/flows/RefreshTask.cpp @@ -0,0 +1,144 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "RefreshTask.h" +#include "../MojangAccount.h" + +#include +#include +#include +#include + +#include + +RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account) +{ +} + +QJsonObject RefreshTask::getRequestContent() const +{ + /* + * { + * "clientToken": "client identifier" + * "accessToken": "current access token to be refreshed" + * "selectedProfile": // specifying this causes errors + * { + * "id": "profile ID" + * "name": "profile name" + * } + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + req.insert("clientToken", m_account->m_clientToken); + req.insert("accessToken", m_account->m_accessToken); + /* + { + auto currentProfile = m_account->currentProfile(); + QJsonObject profile; + profile.insert("id", currentProfile->id()); + profile.insert("name", currentProfile->name()); + req.insert("selectedProfile", profile); + } + */ + req.insert("requestUser", true); + + return req; +} + +void RefreshTask::processResponse(QJsonObject responseData) +{ + // Read the response data. We need to get the client token, access token, and the selected + // profile. + qDebug() << "Processing authentication response."; + + // qDebug() << responseData; + // If we already have a client token, make sure the one the server gave us matches our + // existing one. + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) + { + // Fail if the server gave us an empty client token + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + return; + } + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) + { + changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + return; + } + + // Now, we set the access token. + qDebug() << "Getting new access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) + { + // Fail if the server didn't give us an access token. + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + return; + } + + // we validate that the server responded right. (our current profile = returned current + // profile) + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (m_account->currentProfile()->id != currentProfileId) + { + changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected.")); + return; + } + + // this is what the vanilla launcher passes to the userProperties launch param + if (responseData.contains("user")) + { + User u; + auto obj = responseData.value("user").toObject(); + u.id = obj.value("id").toString(); + auto propArray = obj.value("properties").toArray(); + for (auto prop : propArray) + { + auto propTuple = prop.toObject(); + auto name = propTuple.value("name").toString(); + auto value = propTuple.value("value").toString(); + u.properties.insert(name, value); + } + m_account->m_user = u; + } + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading refresh response."; + // Reset the access token. + m_account->m_accessToken = accessToken; + changeState(STATE_SUCCEEDED); +} + +QString RefreshTask::getEndpoint() const +{ + return "refresh"; +} + +QString RefreshTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_SENDING_REQUEST: + return tr("Refreshing login token..."); + case STATE_PROCESSING_RESPONSE: + return tr("Refreshing login token: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/logic/minecraft/auth/flows/RefreshTask.h b/logic/minecraft/auth/flows/RefreshTask.h new file mode 100644 index 00000000..17714b4f --- /dev/null +++ b/logic/minecraft/auth/flows/RefreshTask.h @@ -0,0 +1,44 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include +#include +#include + +/** + * The authenticate task takes a MojangAccount with a possibly timed-out access token + * and attempts to authenticate with Mojang's servers. + * If successful, it will set the new access token. The token is considered validated. + */ +class RefreshTask : public YggdrasilTask +{ + Q_OBJECT +public: + RefreshTask(MojangAccount * account); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; +}; + diff --git a/logic/minecraft/auth/flows/ValidateTask.cpp b/logic/minecraft/auth/flows/ValidateTask.cpp new file mode 100644 index 00000000..4deceb6a --- /dev/null +++ b/logic/minecraft/auth/flows/ValidateTask.cpp @@ -0,0 +1,61 @@ + +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ValidateTask.h" +#include "../MojangAccount.h" + +#include +#include +#include +#include + +#include + +ValidateTask::ValidateTask(MojangAccount * account, QObject *parent) + : YggdrasilTask(account, parent) +{ +} + +QJsonObject ValidateTask::getRequestContent() const +{ + QJsonObject req; + req.insert("accessToken", m_account->m_accessToken); + return req; +} + +void ValidateTask::processResponse(QJsonObject responseData) +{ + // Assume that if processError wasn't called, then the request was successful. + changeState(YggdrasilTask::STATE_SUCCEEDED); +} + +QString ValidateTask::getEndpoint() const +{ + return "validate"; +} + +QString ValidateTask::getStateMessage() const +{ + switch (m_state) + { + case YggdrasilTask::STATE_SENDING_REQUEST: + return tr("Validating access token: Sending request..."); + case YggdrasilTask::STATE_PROCESSING_RESPONSE: + return tr("Validating access token: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/logic/minecraft/auth/flows/ValidateTask.h b/logic/minecraft/auth/flows/ValidateTask.h new file mode 100644 index 00000000..77d628a0 --- /dev/null +++ b/logic/minecraft/auth/flows/ValidateTask.h @@ -0,0 +1,47 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME: + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include +#include +#include + +/** + * The validate task takes a MojangAccount and checks to make sure its access token is valid. + */ +class ValidateTask : public YggdrasilTask +{ + Q_OBJECT +public: + ValidateTask(MojangAccount *account, QObject *parent = 0); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; + +private: +}; diff --git a/logic/minecraft/forge/ForgeInstaller.cpp b/logic/minecraft/forge/ForgeInstaller.cpp new file mode 100644 index 00000000..a9283e5d --- /dev/null +++ b/logic/minecraft/forge/ForgeInstaller.cpp @@ -0,0 +1,442 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ForgeInstaller.h" +#include "ForgeVersionList.h" + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/GradleSpecifier.h" +#include "net/HttpMetaCache.h" +#include "tasks/Task.h" +#include "minecraft/onesix/OneSixInstance.h" +#include "minecraft/VersionFilterData.h" +#include "Env.h" +#include "Exception.h" +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +ForgeInstaller::ForgeInstaller() : BaseInstaller() +{ +} + +void ForgeInstaller::prepare(const QString &filename, const QString &universalUrl) +{ + std::shared_ptr newVersion; + m_universal_url = universalUrl; + + QuaZip zip(filename); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + // read the install profile + if (!zip.setCurrentFile("install_profile.json")) + return; + + QJsonParseError jsonError; + if (!file.open(QIODevice::ReadOnly)) + return; + QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &jsonError); + file.close(); + if (jsonError.error != QJsonParseError::NoError) + return; + + if (!jsonDoc.isObject()) + return; + + QJsonObject root = jsonDoc.object(); + + auto installVal = root.value("install"); + auto versionInfoVal = root.value("versionInfo"); + if (!installVal.isObject() || !versionInfoVal.isObject()) + return; + + // read the forge version info + { + newVersion = MinecraftProfile::fromJson(versionInfoVal.toObject()); + if (!newVersion) + return; + } + + QJsonObject installObj = installVal.toObject(); + QString libraryName = installObj.value("path").toString(); + internalPath = installObj.value("filePath").toString(); + m_forgeVersionString = installObj.value("version").toString().remove("Forge").trimmed(); + + // where do we put the library? decode the mojang path + GradleSpecifier lib(libraryName); + + auto cacheentry = ENV.metacache()->resolveEntry("libraries", lib.toPath()); + finalPath = "libraries/" + lib.toPath(); + if (!FS::ensureFilePathExists(finalPath)) + return; + + if (!zip.setCurrentFile(internalPath)) + return; + if (!file.open(QIODevice::ReadOnly)) + return; + { + QByteArray data = file.readAll(); + // extract file + QSaveFile extraction(finalPath); + if (!extraction.open(QIODevice::WriteOnly)) + return; + if (extraction.write(data) != data.size()) + return; + if (!extraction.commit()) + return; + QCryptographicHash md5sum(QCryptographicHash::Md5); + md5sum.addData(data); + + cacheentry->stale = false; + cacheentry->md5sum = md5sum.result().toHex().constData(); + ENV.metacache()->updateEntry(cacheentry); + } + file.close(); + + m_forge_json = newVersion; + m_forge_json->id = installObj.value("minecraft").toString(); +} + +bool ForgeInstaller::add(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + + QJsonObject obj; + obj.insert("order", 5); + + if (!m_forge_json) + return false; + int sliding_insert_window = 0; + { + QJsonArray librariesPlus; + // A blacklist + QSet blacklist{"authlib", "realms"}; + // + QList xzlist{"org.scala-lang", "com.typesafe"}; + // for each library in the version we are adding (except for the blacklisted) + for (auto lib : m_forge_json->libraries) + { + QString libName = lib->artifactId(); + QString rawName = lib->rawName(); + + // ignore lwjgl libraries. + if (g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + continue; + // ignore other blacklisted (realms, authlib) + if (blacklist.contains(libName)) + continue; + + // WARNING: This could actually break. + // if this is the actual forge lib, set an absolute url for the download + if (m_forge_version->type == ForgeVersion::Gradle) + { + if (libName == "forge") + { + lib->setClassifier("universal"); + } + else if (libName == "minecraftforge") + { + QString forgeCoord("net.minecraftforge:forge:%1:universal"); + // using insane form of the MC version... + QString longVersion = + m_forge_version->mcver + "-" + m_forge_version->jobbuildver; + GradleSpecifier spec(forgeCoord.arg(longVersion)); + lib->setRawName(spec); + } + } + else + { + if (libName.contains("minecraftforge")) + { + lib->setAbsoluteUrl(m_universal_url); + } + } + + // WARNING: This could actually break. + // mark bad libraries based on the xzlist above + for (auto entry : xzlist) + { + qDebug() << "Testing " << rawName << " : " << entry; + if (rawName.startsWith(entry)) + { + lib->setHint("forge-pack-xz"); + break; + } + } + + QJsonObject libObj = lib->toJson(); + + bool found = false; + bool equals = false; + // find an entry that matches this one + for (auto tolib : to->getMinecraftProfile()->vanillaLibraries) + { + if (tolib->artifactId() != libName) + continue; + found = true; + if (tolib->toJson() == libObj) + { + equals = true; + } + // replace lib + libObj.insert("insert", QString("replace")); + break; + } + if (equals) + { + continue; + } + if (!found) + { + // add lib + libObj.insert("insert", QString("prepend")); + if (lib->artifactId() == "minecraftforge" || lib->artifactId() == "forge") + { + libObj.insert("MMC-depend", QString("hard")); + } + sliding_insert_window++; + } + librariesPlus.prepend(libObj); + } + obj.insert("+libraries", librariesPlus); + obj.insert("mainClass", m_forge_json->mainClass); + QString args = m_forge_json->minecraftArguments; + QStringList tweakers; + { + QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)"); + QRegularExpressionMatch match = expression.match(args); + while (match.hasMatch()) + { + tweakers.append(match.captured(1)); + args.remove(match.capturedStart(), match.capturedLength()); + match = expression.match(args); + } + } + if (!args.isEmpty() && args != to->getMinecraftProfile()->vanillaMinecraftArguments) + { + obj.insert("minecraftArguments", args); + } + if (!tweakers.isEmpty()) + { + obj.insert("+tweakers", QJsonArray::fromStringList(tweakers)); + } + if (!m_forge_json->processArguments.isEmpty() && + m_forge_json->processArguments != to->getMinecraftProfile()->vanillaProcessArguments) + { + obj.insert("processArguments", m_forge_json->processArguments); + } + } + + obj.insert("name", QString("Forge")); + obj.insert("fileId", id()); + obj.insert("version", m_forgeVersionString); + obj.insert("mcVersion", to->intendedVersionId()); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(QJsonDocument(obj).toJson()); + file.close(); + + return true; +} + +bool ForgeInstaller::addLegacy(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + auto entry = ENV.metacache()->resolveEntry("minecraftforge", m_forge_version->filename()); + finalPath = FS::PathCombine(to->jarModsDir(), m_forge_version->filename()); + if (!FS::ensureFilePathExists(finalPath)) + { + return false; + } + if (!QFile::copy(entry->getFullPath(), finalPath)) + { + return false; + } + QJsonObject obj; + obj.insert("order", 5); + { + QJsonArray jarmodsPlus; + { + QJsonObject libObj; + libObj.insert("name", m_forge_version->universal_filename); + jarmodsPlus.append(libObj); + } + obj.insert("+jarMods", jarmodsPlus); + } + + obj.insert("name", QString("Forge")); + obj.insert("fileId", id()); + obj.insert("version", m_forge_version->jobbuildver); + obj.insert("mcVersion", to->intendedVersionId()); + if (g_VersionFilterData.fmlLibsMapping.contains(m_forge_version->mcver)) + { + QJsonArray traitsPlus; + traitsPlus.append(QString("legacyFML")); + obj.insert("+traits", traitsPlus); + } + auto fullversion = to->getMinecraftProfile(); + fullversion->remove("net.minecraftforge"); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(QJsonDocument(obj).toJson()); + file.close(); + return true; +} + +class ForgeInstallTask : public Task +{ + Q_OBJECT +public: + ForgeInstallTask(ForgeInstaller *installer, OneSixInstance *instance, + BaseVersionPtr version, QObject *parent = 0) + : Task(parent), m_installer(installer), m_instance(instance), m_version(version) + { + } + +protected: + void executeTask() override + { + setStatus(tr("Installing Forge...")); + ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast(m_version); + if (!forgeVersion) + { + emitFailed(tr("Unknown error occured")); + return; + } + prepare(forgeVersion); + } + void prepare(ForgeVersionPtr forgeVersion) + { + auto entry = ENV.metacache()->resolveEntry("minecraftforge", forgeVersion->filename()); + auto installFunction = [this, entry, forgeVersion]() + { + if (!install(entry, forgeVersion)) + { + qCritical() << "Failure installing Forge"; + emitFailed(tr("Failure to install Forge")); + } + else + { + reload(); + } + }; + + /* + * HACK IF the local non-stale file is too small, mark is as stale + * + * This fixes some problems with bad files acquired because of unhandled HTTP redirects + * in old versions of MultiMC. + */ + if (!entry->stale) + { + QFileInfo localFile(entry->getFullPath()); + if (localFile.size() <= 0x4000) + { + entry->stale = true; + } + } + + if (entry->stale) + { + NetJob *fjob = new NetJob("Forge download"); + fjob->addNetAction(CacheDownload::make(forgeVersion->url(), entry)); + connect(fjob, &NetJob::progress, this, &Task::setProgress); + connect(fjob, &NetJob::status, this, &Task::setStatus); + connect(fjob, &NetJob::failed, [this](QString reason) + { emitFailed(tr("Failure to download Forge:\n%1").arg(reason)); }); + connect(fjob, &NetJob::succeeded, installFunction); + fjob->start(); + } + else + { + installFunction(); + } + } + bool install(const std::shared_ptr &entry, const ForgeVersionPtr &forgeVersion) + { + if (forgeVersion->usesInstaller()) + { + QString forgePath = entry->getFullPath(); + m_installer->prepare(forgePath, forgeVersion->universal_url); + return m_installer->add(m_instance); + } + else + return m_installer->addLegacy(m_instance); + } + void reload() + { + try + { + m_instance->reloadProfile(); + emitSucceeded(); + } + catch (Exception &e) + { + emitFailed(e.cause()); + } + catch (...) + { + emitFailed(tr("Failed to load the version description file for reasons unknown.")); + } + } + +private: + ForgeInstaller *m_installer; + OneSixInstance *m_instance; + BaseVersionPtr m_version; +}; + +Task *ForgeInstaller::createInstallTask(OneSixInstance *instance, + BaseVersionPtr version, QObject *parent) +{ + if (!version) + { + return nullptr; + } + m_forge_version = std::dynamic_pointer_cast(version); + return new ForgeInstallTask(this, instance, version, parent); +} + +#include "ForgeInstaller.moc" diff --git a/logic/minecraft/forge/ForgeInstaller.h b/logic/minecraft/forge/ForgeInstaller.h new file mode 100644 index 00000000..0de762b6 --- /dev/null +++ b/logic/minecraft/forge/ForgeInstaller.h @@ -0,0 +1,52 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseInstaller.h" + +#include +#include + +#include "multimc_logic_export.h" + +class MinecraftProfile; +class ForgeInstallTask; +struct ForgeVersion; + +class MULTIMC_LOGIC_EXPORT ForgeInstaller : public BaseInstaller +{ + friend class ForgeInstallTask; +public: + ForgeInstaller(); + virtual ~ForgeInstaller(){} + virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override; + virtual QString id() const override { return "net.minecraftforge"; } + +protected: + void prepare(const QString &filename, const QString &universalUrl); + bool add(OneSixInstance *to) override; + bool addLegacy(OneSixInstance *to); + +private: + // the parsed version json, read from the installer + std::shared_ptr m_forge_json; + // the actual forge version + std::shared_ptr m_forge_version; + QString internalPath; + QString finalPath; + QString m_forgeVersionString; + QString m_universal_url; +}; diff --git a/logic/minecraft/forge/ForgeMirror.h b/logic/minecraft/forge/ForgeMirror.h new file mode 100644 index 00000000..2518dffe --- /dev/null +++ b/logic/minecraft/forge/ForgeMirror.h @@ -0,0 +1,10 @@ +#pragma once +#include + +struct ForgeMirror +{ + QString name; + QString logo_url; + QString website_url; + QString mirror_url; +}; \ No newline at end of file diff --git a/logic/minecraft/forge/ForgeMirrors.cpp b/logic/minecraft/forge/ForgeMirrors.cpp new file mode 100644 index 00000000..a2fc2c62 --- /dev/null +++ b/logic/minecraft/forge/ForgeMirrors.cpp @@ -0,0 +1,118 @@ +#include "Env.h" +#include "ForgeMirrors.h" +#include +#include +#include + +ForgeMirrors::ForgeMirrors(QList &libs, NetJobPtr parent_job, + QString mirrorlist) +{ + m_libs = libs; + m_parent_job = parent_job; + m_url = QUrl(mirrorlist); + m_status = Job_NotStarted; +} + +void ForgeMirrors::start() +{ + qDebug() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + auto worker = ENV.qnam(); + QNetworkReply *rep = worker->get(request); + + m_reply.reset(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); +} + +void ForgeMirrors::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Error getting URL:" << m_url.toString().toLocal8Bit() + << "Network error: " << error; + m_status = Job_Failed; +} + +void ForgeMirrors::downloadFinished() +{ + // if the download succeeded + if (m_status != Job_Failed) + { + // nothing went wrong... ? + parseMirrorList(); + return; + } + // else the download failed, we use a fixed list + else + { + m_status = Job_Finished; + m_reply.reset(); + deferToFixedList(); + return; + } +} + +void ForgeMirrors::deferToFixedList() +{ + m_mirrors.clear(); + m_mirrors.append( + {"Minecraft Forge", "http://files.minecraftforge.net/forge_logo.png", + "http://files.minecraftforge.net/", "http://files.minecraftforge.net/maven/"}); + m_mirrors.append({"Creeper Host", + "http://files.minecraftforge.net/forge_logo.png", + "https://www.creeperhost.net/link.php?id=1", + "http://new.creeperrepo.net/forge/maven/"}); + injectDownloads(); + emit succeeded(m_index_within_job); +} + +void ForgeMirrors::parseMirrorList() +{ + m_status = Job_Finished; + auto data = m_reply->readAll(); + m_reply.reset(); + auto dataLines = data.split('\n'); + for(auto line: dataLines) + { + auto elements = line.split('!'); + if (elements.size() == 4) + { + m_mirrors.append({elements[0],elements[1],elements[2],elements[3]}); + } + } + if(!m_mirrors.size()) + deferToFixedList(); + injectDownloads(); + emit succeeded(m_index_within_job); +} + +void ForgeMirrors::injectDownloads() +{ + // shuffle the mirrors randomly + std::random_device rd; + std::mt19937 rng(rd()); + std::shuffle(m_mirrors.begin(), m_mirrors.end(), rng); + + // tell parent to download the libs + for(auto lib: m_libs) + { + lib->setMirrors(m_mirrors); + m_parent_job->addNetAction(lib); + } +} + +void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} + +void ForgeMirrors::downloadReadyRead() +{ +} diff --git a/logic/minecraft/forge/ForgeMirrors.h b/logic/minecraft/forge/ForgeMirrors.h new file mode 100644 index 00000000..0312829b --- /dev/null +++ b/logic/minecraft/forge/ForgeMirrors.h @@ -0,0 +1,61 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "ForgeXzDownload.h" + +#include "net/NetAction.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" + +#include +#include + +typedef std::shared_ptr ForgeMirrorsPtr; + +class ForgeMirrors : public NetAction +{ + Q_OBJECT +public: + QList m_libs; + NetJobPtr m_parent_job; + QList m_mirrors; + +public: + explicit ForgeMirrors(QList &libs, NetJobPtr parent_job, + QString mirrorlist); + static ForgeMirrorsPtr make(QList &libs, NetJobPtr parent_job, + QString mirrorlist) + { + return ForgeMirrorsPtr(new ForgeMirrors(libs, parent_job, mirrorlist)); + } + virtual ~ForgeMirrors(){}; +protected +slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead(); + +private: + void parseMirrorList(); + void deferToFixedList(); + void injectDownloads(); + +public +slots: + virtual void start(); +}; diff --git a/logic/minecraft/forge/ForgeVersion.cpp b/logic/minecraft/forge/ForgeVersion.cpp new file mode 100644 index 00000000..b859a28c --- /dev/null +++ b/logic/minecraft/forge/ForgeVersion.cpp @@ -0,0 +1,55 @@ +#include "ForgeVersion.h" +#include "minecraft/VersionFilterData.h" +#include + +QString ForgeVersion::name() +{ + return "Forge " + jobbuildver; +} + +QString ForgeVersion::descriptor() +{ + return universal_filename; +} + +QString ForgeVersion::typeString() const +{ + if (is_recommended) + return QObject::tr("Recommended"); + return QString(); +} + +bool ForgeVersion::operator<(BaseVersion &a) +{ + ForgeVersion *pa = dynamic_cast(&a); + if (!pa) + return true; + return m_buildnr < pa->m_buildnr; +} + +bool ForgeVersion::operator>(BaseVersion &a) +{ + ForgeVersion *pa = dynamic_cast(&a); + if (!pa) + return false; + return m_buildnr > pa->m_buildnr; +} + +bool ForgeVersion::usesInstaller() +{ + if(installer_url.isEmpty()) + return false; + if(g_VersionFilterData.forgeInstallerBlacklist.contains(mcver)) + return false; + return true; +} + +QString ForgeVersion::filename() +{ + return usesInstaller() ? installer_filename : universal_filename; +} + +QString ForgeVersion::url() +{ + return usesInstaller() ? installer_url : universal_url; +} diff --git a/logic/minecraft/forge/ForgeVersion.h b/logic/minecraft/forge/ForgeVersion.h new file mode 100644 index 00000000..e77d32f1 --- /dev/null +++ b/logic/minecraft/forge/ForgeVersion.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include "BaseVersion.h" + +struct ForgeVersion; +typedef std::shared_ptr ForgeVersionPtr; + +struct ForgeVersion : public BaseVersion +{ + virtual QString descriptor() override; + virtual QString name() override; + virtual QString typeString() const override; + virtual bool operator<(BaseVersion &a) override; + virtual bool operator>(BaseVersion &a) override; + + QString filename(); + QString url(); + + enum + { + Invalid, + Legacy, + Gradle + } type = Invalid; + + bool usesInstaller(); + + int m_buildnr = 0; + QString branch; + QString universal_url; + QString changelog_url; + QString installer_url; + QString jobbuildver; + QString mcver; + QString mcver_sane; + QString universal_filename; + QString installer_filename; + bool is_recommended = false; +}; + +Q_DECLARE_METATYPE(ForgeVersionPtr) diff --git a/logic/minecraft/forge/ForgeVersionList.cpp b/logic/minecraft/forge/ForgeVersionList.cpp new file mode 100644 index 00000000..9b418310 --- /dev/null +++ b/logic/minecraft/forge/ForgeVersionList.cpp @@ -0,0 +1,450 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ForgeVersionList.h" +#include "ForgeVersion.h" + +#include "net/NetJob.h" +#include "net/URLConstants.h" +#include "Env.h" + +#include +#include +#include + +#include + +ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent) +{ +} + +Task *ForgeVersionList::getLoadTask() +{ + return new ForgeListLoadTask(this); +} + +bool ForgeVersionList::isLoaded() +{ + return m_loaded; +} + +const BaseVersionPtr ForgeVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int ForgeVersionList::count() const +{ + return m_vlist.count(); +} + +int ForgeVersionList::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ForgeVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(m_vlist[index.row()]); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case ParentGameVersionRole: + return version->mcver_sane; + + case RecommendedRole: + return version->is_recommended; + + case BranchRole: + return version->branch; + + default: + return QVariant(); + } +} + +QList ForgeVersionList::providesRoles() +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole}; +} + +BaseVersionPtr ForgeVersionList::getLatestStable() const +{ + return BaseVersionPtr(); +} + +void ForgeVersionList::updateListData(QList versions) +{ + beginResetModel(); + m_vlist = versions; + m_loaded = true; + endResetModel(); + // NOW SORT!! + // sort(); +} + +void ForgeVersionList::sortVersions() +{ + // NO-OP for now +} + +ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task() +{ + m_list = vlist; +} + +void ForgeListLoadTask::executeTask() +{ + setStatus(tr("Fetching Forge version lists...")); + auto job = new NetJob("Version index"); + // we do not care if the version is stale or not. + auto forgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "list.json"); + auto gradleForgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "json"); + + // verify by poking the server. + forgeListEntry->stale = true; + gradleForgeListEntry->stale = true; + + job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL), + forgeListEntry)); + job->addNetAction(gradleListDownload = CacheDownload::make( + QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry)); + + connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed())); + connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed())); + + listJob.reset(job); + connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded())); + connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + listJob->start(); +} + +bool ForgeListLoadTask::abort() +{ + return listJob->abort(); +} + +bool ForgeListLoadTask::parseForgeList(QList &out) +{ + QByteArray data; + { + auto dlJob = listDownload; + auto filename = std::dynamic_pointer_cast(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Now, get the array of versions. + if (!root.value("builds").isArray()) + { + emitFailed( + "Error parsing version list JSON: version list object is missing 'builds' array"); + return false; + } + QJsonArray builds = root.value("builds").toArray(); + + for (int i = 0; i < builds.count(); i++) + { + // Load the version info. + if (!builds[i].isObject()) + { + // FIXME: log this somewhere + continue; + } + QJsonObject obj = builds[i].toObject(); + int build_nr = obj.value("build").toDouble(0); + if (!build_nr) + continue; + QJsonArray files = obj.value("files").toArray(); + QString url, jobbuildver, mcver, buildtype, universal_filename; + QString changelog_url, installer_url; + QString installer_filename; + bool valid = false; + for (int j = 0; j < files.count(); j++) + { + if (!files[j].isObject()) + { + continue; + } + QJsonObject file = files[j].toObject(); + buildtype = file.value("buildtype").toString(); + if ((buildtype == "client" || buildtype == "universal") && !valid) + { + mcver = file.value("mcver").toString(); + url = file.value("url").toString(); + jobbuildver = file.value("jobbuildver").toString(); + int lastSlash = url.lastIndexOf('/'); + universal_filename = url.mid(lastSlash + 1); + valid = true; + } + else if (buildtype == "changelog") + { + QString ext = file.value("ext").toString(); + if (ext.isEmpty()) + { + continue; + } + changelog_url = file.value("url").toString(); + } + else if (buildtype == "installer") + { + installer_url = file.value("url").toString(); + int lastSlash = installer_url.lastIndexOf('/'); + installer_filename = installer_url.mid(lastSlash + 1); + } + } + if (valid) + { + // Now, we construct the version object and add it to the list. + std::shared_ptr fVersion(new ForgeVersion()); + fVersion->universal_url = url; + fVersion->changelog_url = changelog_url; + fVersion->installer_url = installer_url; + fVersion->jobbuildver = jobbuildver; + fVersion->mcver = fVersion->mcver_sane = mcver; + fVersion->installer_filename = installer_filename; + fVersion->universal_filename = universal_filename; + fVersion->m_buildnr = build_nr; + fVersion->type = ForgeVersion::Legacy; + out.append(fVersion); + } + } + + return true; +} + +bool ForgeListLoadTask::parseForgeGradleList(QList &out) +{ + QMap> lookup; + QByteArray data; + { + auto dlJob = gradleListDownload; + auto filename = std::dynamic_pointer_cast(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing gradle version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // we probably could hard code these, but it might still be worth doing it this way + const QString webpath = root.value("webpath").toString(); + const QString artifact = root.value("artifact").toString(); + + QJsonObject numbers = root.value("number").toObject(); + for (auto it = numbers.begin(); it != numbers.end(); ++it) + { + QJsonObject number = it.value().toObject(); + std::shared_ptr fVersion(new ForgeVersion()); + fVersion->m_buildnr = number.value("build").toDouble(); + if(fVersion->m_buildnr >= 953 && fVersion->m_buildnr <= 965) + { + qDebug() << fVersion->m_buildnr; + } + fVersion->jobbuildver = number.value("version").toString(); + fVersion->branch = number.value("branch").toString(""); + fVersion->mcver = number.value("mcversion").toString(); + fVersion->universal_filename = ""; + fVersion->installer_filename = ""; + // HACK: here, we fix the minecraft version used by forge. + // HACK: this will inevitably break (later) + // FIXME: replace with a dictionary + fVersion->mcver_sane = fVersion->mcver; + fVersion->mcver_sane.replace("_pre", "-pre"); + + QString universal_filename, installer_filename; + QJsonArray files = number.value("files").toArray(); + for (auto fIt = files.begin(); fIt != files.end(); ++fIt) + { + // TODO with gradle we also get checksums, use them + QJsonArray file = (*fIt).toArray(); + if (file.size() < 3) + { + continue; + } + + QString extension = file.at(0).toString(); + QString part = file.at(1).toString(); + QString checksum = file.at(2).toString(); + + // insane form of mcver is used here + QString longVersion = fVersion->mcver + "-" + fVersion->jobbuildver; + if (!fVersion->branch.isEmpty()) + { + longVersion = longVersion + "-" + fVersion->branch; + } + QString filename = artifact + "-" + longVersion + "-" + part + "." + extension; + + QString url = QString("%1/%2/%3") + .arg(webpath) + .arg(longVersion) + .arg(filename); + + if (part == "installer") + { + fVersion->installer_url = url; + installer_filename = filename; + } + else if (part == "universal") + { + fVersion->universal_url = url; + universal_filename = filename; + } + else if (part == "changelog") + { + fVersion->changelog_url = url; + } + } + if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty()) + { + continue; + } + fVersion->universal_filename = universal_filename; + fVersion->installer_filename = installer_filename; + fVersion->type = ForgeVersion::Gradle; + out.append(fVersion); + lookup[fVersion->m_buildnr] = fVersion; + } + QJsonObject promos = root.value("promos").toObject(); + for (auto it = promos.begin(); it != promos.end(); ++it) + { + QString key = it.key(); + int build = it.value().toInt(); + QRegularExpression regexp("^(?[0-9]+(.[0-9]+)*)-(?