From 253067c782955380bbf66ac0475dc954375b1ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 17 Aug 2013 13:40:51 +0200 Subject: Move all the things (YES. Move them.) Also, implemented some basic modlist logic, to be wired up. --- logic/BaseInstance.cpp | 181 ++++++++++++++ logic/BaseInstance.h | 145 +++++++++++ logic/BaseInstance_p.h | 14 ++ logic/BaseUpdate.cpp | 13 + logic/BaseUpdate.h | 49 ++++ logic/CMakeLists.txt | 24 ++ logic/IconListModel.cpp | 163 ++++++++++++ logic/IconListModel.h | 33 +++ logic/InstanceFactory.cpp | 113 +++++++++ logic/InstanceFactory.h | 80 ++++++ logic/InstanceVersion.h | 68 +++++ logic/LegacyForge.cpp | 57 +++++ logic/LegacyForge.h | 25 ++ logic/LegacyInstance.cpp | 274 ++++++++++++++++++++ logic/LegacyInstance.h | 95 +++++++ logic/LegacyInstance_p.h | 15 ++ logic/LegacyUpdate.cpp | 381 ++++++++++++++++++++++++++++ logic/LegacyUpdate.h | 67 +++++ logic/MinecraftProcess.cpp | 169 +++++++++++++ logic/MinecraftProcess.h | 86 +++++++ logic/MinecraftVersion.h | 76 ++++++ logic/Mod.cpp | 264 ++++++++++++++++++++ logic/Mod.h | 71 ++++++ logic/ModList.cpp | 472 +++++++++++++++++++++++++++++++++++ logic/ModList.h | 67 +++++ logic/NostalgiaInstance.cpp | 12 + logic/NostalgiaInstance.h | 10 + logic/OneSixAssets.cpp | 162 ++++++++++++ logic/OneSixAssets.h | 22 ++ logic/OneSixInstance.cpp | 218 ++++++++++++++++ logic/OneSixInstance.h | 34 +++ logic/OneSixInstance_p.h | 9 + logic/OneSixUpdate.cpp | 169 +++++++++++++ logic/OneSixUpdate.h | 54 ++++ logic/OneSixVersion.cpp | 132 ++++++++++ logic/OneSixVersion.h | 212 ++++++++++++++++ logic/VersionFactory.cpp | 195 +++++++++++++++ logic/VersionFactory.h | 24 ++ logic/lists/InstVersionList.cpp | 129 ++++++++++ logic/lists/InstVersionList.h | 121 +++++++++ logic/lists/InstanceList.cpp | 232 +++++++++++++++++ logic/lists/InstanceList.h | 91 +++++++ logic/lists/LwjglVersionList.cpp | 206 +++++++++++++++ logic/lists/LwjglVersionList.h | 117 +++++++++ logic/lists/MinecraftVersionList.cpp | 300 ++++++++++++++++++++++ logic/lists/MinecraftVersionList.h | 83 ++++++ logic/net/DownloadJob.cpp | 138 ++++++++++ logic/net/DownloadJob.h | 51 ++++ logic/net/JobQueue.h | 180 +++++++++++++ logic/net/NetWorker.cpp | 12 + logic/net/NetWorker.h | 20 ++ logic/tasks/LoginTask.cpp | 111 ++++++++ logic/tasks/LoginTask.h | 58 +++++ logic/tasks/Task.cpp | 85 +++++++ logic/tasks/Task.h | 65 +++++ 55 files changed, 6254 insertions(+) create mode 100644 logic/BaseInstance.cpp create mode 100644 logic/BaseInstance.h create mode 100644 logic/BaseInstance_p.h create mode 100644 logic/BaseUpdate.cpp create mode 100644 logic/BaseUpdate.h create mode 100644 logic/CMakeLists.txt create mode 100644 logic/IconListModel.cpp create mode 100644 logic/IconListModel.h create mode 100644 logic/InstanceFactory.cpp create mode 100644 logic/InstanceFactory.h create mode 100644 logic/InstanceVersion.h create mode 100644 logic/LegacyForge.cpp create mode 100644 logic/LegacyForge.h create mode 100644 logic/LegacyInstance.cpp create mode 100644 logic/LegacyInstance.h create mode 100644 logic/LegacyInstance_p.h create mode 100644 logic/LegacyUpdate.cpp create mode 100644 logic/LegacyUpdate.h create mode 100644 logic/MinecraftProcess.cpp create mode 100644 logic/MinecraftProcess.h create mode 100644 logic/MinecraftVersion.h create mode 100644 logic/Mod.cpp create mode 100644 logic/Mod.h create mode 100644 logic/ModList.cpp create mode 100644 logic/ModList.h create mode 100644 logic/NostalgiaInstance.cpp create mode 100644 logic/NostalgiaInstance.h create mode 100644 logic/OneSixAssets.cpp create mode 100644 logic/OneSixAssets.h create mode 100644 logic/OneSixInstance.cpp create mode 100644 logic/OneSixInstance.h create mode 100644 logic/OneSixInstance_p.h create mode 100644 logic/OneSixUpdate.cpp create mode 100644 logic/OneSixUpdate.h create mode 100644 logic/OneSixVersion.cpp create mode 100644 logic/OneSixVersion.h create mode 100644 logic/VersionFactory.cpp create mode 100644 logic/VersionFactory.h create mode 100644 logic/lists/InstVersionList.cpp create mode 100644 logic/lists/InstVersionList.h create mode 100644 logic/lists/InstanceList.cpp create mode 100644 logic/lists/InstanceList.h create mode 100644 logic/lists/LwjglVersionList.cpp create mode 100644 logic/lists/LwjglVersionList.h create mode 100644 logic/lists/MinecraftVersionList.cpp create mode 100644 logic/lists/MinecraftVersionList.h create mode 100644 logic/net/DownloadJob.cpp create mode 100644 logic/net/DownloadJob.h create mode 100644 logic/net/JobQueue.h create mode 100644 logic/net/NetWorker.cpp create mode 100644 logic/net/NetWorker.h create mode 100644 logic/tasks/LoginTask.cpp create mode 100644 logic/tasks/LoginTask.h create mode 100644 logic/tasks/Task.cpp create mode 100644 logic/tasks/Task.h (limited to 'logic') diff --git a/logic/BaseInstance.cpp b/logic/BaseInstance.cpp new file mode 100644 index 00000000..c2df34e1 --- /dev/null +++ b/logic/BaseInstance.cpp @@ -0,0 +1,181 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseInstance.h" +#include "BaseInstance_p.h" + +#include + +#include "inisettingsobject.h" +#include "setting.h" +#include "overridesetting.h" + +#include "pathutils.h" +#include "lists/MinecraftVersionList.h" + + +BaseInstance::BaseInstance( BaseInstancePrivate* d_in, + const QString& rootDir, + SettingsObject* settings_obj, + QObject* parent + ) +:inst_d(d_in), QObject(parent) +{ + I_D(BaseInstance); + d->m_settings = settings_obj; + d->m_rootDir = rootDir; + + settings().registerSetting(new Setting("name", "Unnamed Instance")); + settings().registerSetting(new Setting("iconKey", "default")); + settings().registerSetting(new Setting("notes", "")); + settings().registerSetting(new Setting("lastLaunchTime", 0)); + + // Java Settings + settings().registerSetting(new Setting("OverrideJava", false)); + settings().registerSetting(new OverrideSetting("JavaPath", globalSettings->getSetting("JavaPath"))); + settings().registerSetting(new OverrideSetting("JvmArgs", globalSettings->getSetting("JvmArgs"))); + + // Custom Commands + settings().registerSetting(new Setting("OverrideCommands", false)); + settings().registerSetting(new OverrideSetting("PreLaunchCommand", globalSettings->getSetting("PreLaunchCommand"))); + settings().registerSetting(new OverrideSetting("PostExitCommand", globalSettings->getSetting("PostExitCommand"))); + + // Window Size + settings().registerSetting(new Setting("OverrideWindow", false)); + settings().registerSetting(new OverrideSetting("LaunchMaximized", globalSettings->getSetting("LaunchMaximized"))); + settings().registerSetting(new OverrideSetting("MinecraftWinWidth", globalSettings->getSetting("MinecraftWinWidth"))); + settings().registerSetting(new OverrideSetting("MinecraftWinHeight", globalSettings->getSetting("MinecraftWinHeight"))); + + // Memory + settings().registerSetting(new Setting("OverrideMemory", false)); + settings().registerSetting(new OverrideSetting("MinMemAlloc", globalSettings->getSetting("MinMemAlloc"))); + settings().registerSetting(new OverrideSetting("MaxMemAlloc", globalSettings->getSetting("MaxMemAlloc"))); + + // Auto login + settings().registerSetting(new Setting("OverrideLogin", false)); + settings().registerSetting(new OverrideSetting("AutoLogin", globalSettings->getSetting("AutoLogin"))); + + // Console + settings().registerSetting(new Setting("OverrideConsole", false)); + settings().registerSetting(new OverrideSetting("ShowConsole", globalSettings->getSetting("ShowConsole"))); + settings().registerSetting(new OverrideSetting("AutoCloseConsole", globalSettings->getSetting("AutoCloseConsole"))); +} + +QString BaseInstance::id() const +{ + return QFileInfo(instanceRoot()).fileName(); +} + +QString BaseInstance::instanceType() const +{ + I_D(BaseInstance); + return d->m_settings->get("InstanceType").toString(); +} + + +QString BaseInstance::instanceRoot() const +{ + I_D(BaseInstance); + return d->m_rootDir; +} + +QString BaseInstance::minecraftRoot() const +{ + QFileInfo mcDir(PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(PathCombine(instanceRoot(), ".minecraft")); + + if (dotMCDir.exists() && !mcDir.exists()) + return dotMCDir.filePath(); + else + return mcDir.filePath(); +} + +InstanceList *BaseInstance::instList() const +{ + if (parent()->inherits("InstanceList")) + return (InstanceList *)parent(); + else + return NULL; +} + +InstVersionList *BaseInstance::versionList() const +{ + return &MinecraftVersionList::getMainList(); +} + +SettingsObject &BaseInstance::settings() const +{ + I_D(BaseInstance); + return *d->m_settings; +} + +qint64 BaseInstance::lastLaunch() const +{ + I_D(BaseInstance); + return d->m_settings->get ( "lastLaunchTime" ).value(); +} +void BaseInstance::setLastLaunch ( qint64 val ) +{ + I_D(BaseInstance); + d->m_settings->set ( "lastLaunchTime", val ); + emit propertiesChanged ( this ); +} + +void BaseInstance::setGroup ( QString val ) +{ + I_D(BaseInstance); + d->m_group = val; + emit propertiesChanged ( this ); +} +QString BaseInstance::group() const +{ + I_D(BaseInstance); + return d->m_group; +} + +void BaseInstance::setNotes ( QString val ) +{ + I_D(BaseInstance); + d->m_settings->set ( "notes", val ); +} +QString BaseInstance::notes() const +{ + I_D(BaseInstance); + return d->m_settings->get ( "notes" ).toString(); +} + +void BaseInstance::setIconKey ( QString val ) +{ + I_D(BaseInstance); + d->m_settings->set ( "iconKey", val ); + emit propertiesChanged ( this ); +} +QString BaseInstance::iconKey() const +{ + I_D(BaseInstance); + return d->m_settings->get ( "iconKey" ).toString(); +} + +void BaseInstance::setName ( QString val ) +{ + I_D(BaseInstance); + d->m_settings->set ( "name", val ); + emit propertiesChanged ( this ); +} +QString BaseInstance::name() const +{ + I_D(BaseInstance); + return d->m_settings->get ( "name" ).toString(); +} diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h new file mode 100644 index 00000000..8b5de6f5 --- /dev/null +++ b/logic/BaseInstance.h @@ -0,0 +1,145 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +#include "inifile.h" +#include "lists/InstVersionList.h" + +class QDialog; +class BaseUpdate; +class MinecraftProcess; +class OneSixUpdate; +class InstanceList; +class BaseInstancePrivate; + +/*! + * \brief Base class for instances. + * This class implements many functions that are common between instances and + * provides a standard interface for all instances. + * + * To create a new instance type, create a new class inheriting from this class + * and implement the pure virtual functions. + */ +class BaseInstance : public QObject +{ + Q_OBJECT +protected: + /// no-touchy! + BaseInstance(BaseInstancePrivate * d, const QString &rootDir, SettingsObject * settings, QObject *parent = 0); +public: + /// virtual destructor to make sure the destruction is COMPLETE + virtual ~BaseInstance() {}; + + /// The instance's ID. The ID SHALL be determined by MMC internally. The ID IS guaranteed to be unique. + QString id() const; + + /// get the type of this instance + QString instanceType() const; + + /// Path to the instance's root directory. + QString instanceRoot() const; + + /// Path to the instance's minecraft directory. + QString minecraftRoot() const; + + QString name() const; + void setName(QString val); + + QString iconKey() const; + void setIconKey(QString val); + + QString notes() const; + void setNotes(QString val); + + QString group() const; + void setGroup(QString val); + + virtual QString intendedVersionId() const = 0; + virtual bool setIntendedVersionId(QString version) = 0; + + /*! + * The instance's current version. + * This value represents the instance's current version. If this value is + * different from the intendedVersion, the instance should be updated. + * \warning Don't change this value unless you know what you're doing. + */ + virtual QString currentVersionId() const = 0; + //virtual void setCurrentVersionId(QString val) = 0; + + /*! + * Whether or not Minecraft should be downloaded when the instance is launched. + */ + virtual bool shouldUpdate() const = 0; + virtual void setShouldUpdate(bool val) = 0; + + /** + * Gets the time that the instance was last launched. + * Stored in milliseconds since epoch. + */ + qint64 lastLaunch() const; + /// Sets the last launched time to 'val' milliseconds since epoch + void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch()); + + /*! + * \brief Gets the instance list that this instance is a part of. + * Returns NULL if this instance is not in a list + * (the parent is not an InstanceList). + * \return A pointer to the InstanceList containing this instance. + */ + InstanceList *instList() const; + + /*! + * \brief Gets a pointer to this instance's version list. + * \return A pointer to the available version list for this instance. + */ + virtual InstVersionList *versionList() const; + + /*! + * \brief Gets this instance's settings object. + * This settings object stores instance-specific settings. + * \return A pointer to this instance's settings object. + */ + virtual SettingsObject &settings() const; + + /// returns a valid update task if update is needed, NULL otherwise + virtual BaseUpdate* doUpdate() = 0; + + /// returns a valid minecraft process, ready for launch + virtual MinecraftProcess* prepareForLaunch(QString user, QString session) = 0; + + /// do any necessary cleanups after the instance finishes. also runs before 'prepareForLaunch' + virtual void cleanupAfterRun() = 0; + + /// create a mod edit dialog for the instance + virtual QSharedPointer createModEditDialog ( QWidget* parent ) = 0; +signals: + /*! + * \brief Signal emitted when properties relevant to the instance view change + */ + void propertiesChanged(BaseInstance * inst); + +protected: + QSharedPointer inst_d; +}; + +// pointer for lazy people +typedef QSharedPointer InstancePtr; + diff --git a/logic/BaseInstance_p.h b/logic/BaseInstance_p.h new file mode 100644 index 00000000..a30916a4 --- /dev/null +++ b/logic/BaseInstance_p.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +class BaseInstance; + +#define I_D(Class) Class##Private * const d = (Class##Private * const) inst_d.data() + +struct BaseInstancePrivate +{ + QString m_rootDir; + QString m_group; + SettingsObject *m_settings; +}; \ No newline at end of file diff --git a/logic/BaseUpdate.cpp b/logic/BaseUpdate.cpp new file mode 100644 index 00000000..b086ab14 --- /dev/null +++ b/logic/BaseUpdate.cpp @@ -0,0 +1,13 @@ +#include "BaseUpdate.h" + +BaseUpdate::BaseUpdate ( BaseInstance* inst, QObject* parent ) : Task ( parent ) +{ + m_inst = inst; +} + +void BaseUpdate::updateDownloadProgress(qint64 current, qint64 total) +{ + // The progress on the current file is current / total + float currentDLProgress = (float) current / (float) total; + setProgress((int)(currentDLProgress * 100)); // convert to percentage +} \ No newline at end of file diff --git a/logic/BaseUpdate.h b/logic/BaseUpdate.h new file mode 100644 index 00000000..d1e7b735 --- /dev/null +++ b/logic/BaseUpdate.h @@ -0,0 +1,49 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "net/DownloadJob.h" + +#include "tasks/Task.h" + +class MinecraftVersion; +class BaseInstance; + +/*! + * The game update task is the task that handles downloading instances' files. + */ +class BaseUpdate : public Task +{ + Q_OBJECT +public: + explicit BaseUpdate(BaseInstance *inst, QObject *parent = 0); + + virtual void executeTask() = 0; + +protected slots: + //virtual void error(const QString &msg); + void updateDownloadProgress(qint64 current, qint64 total); + +protected: + JobListQueue download_queue; + BaseInstance *m_inst; +}; + + diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt new file mode 100644 index 00000000..b1eacced --- /dev/null +++ b/logic/CMakeLists.txt @@ -0,0 +1,24 @@ +project(libMultiMC) + +set(CMAKE_AUTOMOC ON) + +# Find Qt +find_package(Qt5Core REQUIRED) +find_package(Qt5Network REQUIRED) +find_package(Qt5Xml REQUIRED) + +# Include Qt headers. +include_directories(${Qt5Base_INCLUDE_DIRS}) +include_directories(${Qt5Network_INCLUDE_DIRS}) + +# Include utility library. +include_directories(${CMAKE_SOURCE_DIR}/libutil/include) + +# Include settings library. +include_directories(${CMAKE_SOURCE_DIR}/libsettings/include) + +SET(LIBINST_HEADERS + +) + + diff --git a/logic/IconListModel.cpp b/logic/IconListModel.cpp new file mode 100644 index 00000000..2d2fb6cf --- /dev/null +++ b/logic/IconListModel.cpp @@ -0,0 +1,163 @@ +#include "IconListModel.h" +#include +#include +#include +#include + +#define MAX_SIZE 1024 +IconList* IconList::m_Instance = 0; +QMutex IconList::mutex; + +struct entry +{ + QString key; + QString name; + QIcon icon; + bool is_builtin; +}; + +class Private : public QObject +{ + Q_OBJECT +public: + QMap index; + QVector icons; + Private() + { + } +}; + + +IconList::IconList() : QAbstractListModel(), d(new Private()) +{ + QDir instance_icons(":/icons/instances/"); + auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); + for(auto file_info: file_info_list) + { + QString key = file_info.baseName(); + addIcon(key, key, file_info.absoluteFilePath(), true); + } + + // FIXME: get from settings + ensurePathExists("icons/"); + QDir user_icons("icons/"); + file_info_list = user_icons.entryInfoList(QDir::Files, QDir::Name); + for(auto file_info: file_info_list) + { + QString filename = file_info.absoluteFilePath(); + QString key = file_info.baseName(); + addIcon(key, key, filename); + } +} + +IconList::~IconList() +{ + delete d; + d = nullptr; +} + +QVariant IconList::data ( const QModelIndex& index, int role ) const +{ + if(!index.isValid()) + return QVariant(); + + int row = index.row(); + + if(row < 0 || row >= d->icons.size()) + return QVariant(); + + switch(role) + { + case Qt::DecorationRole: + return d->icons[row].icon; + case Qt::DisplayRole: + return d->icons[row].name; + case Qt::UserRole: + return d->icons[row].key; + default: + return QVariant(); + } +} + +int IconList::rowCount ( const QModelIndex& parent ) const +{ + return d->icons.size(); +} + +bool IconList::addIcon ( QString key, QString name, QString path, bool is_builtin ) +{ + auto iter = d->index.find(key); + if(iter != d->index.end()) + { + if(d->icons[*iter].is_builtin) return false; + + QIcon icon(path); + if(icon.isNull()) return false; + + // replace the icon + d->icons[*iter] = {key, name, icon, is_builtin}; + return true; + } + else + { + QIcon icon(path); + if(icon.isNull()) return false; + + // add a new icon + d->icons.push_back({key, name, icon, is_builtin}); + d->index[key] = d->icons.size() - 1; + return true; + } +} + + +QIcon IconList::getIcon ( QString key ) +{ + int icon_index = getIconIndex(key); + + if(icon_index != -1) + return d->icons[icon_index].icon; + + // Fallback for icons that don't exist. + icon_index = getIconIndex("infinity"); + + if(icon_index != -1) + return d->icons[icon_index].icon; + return QIcon(); +} + +int IconList::getIconIndex ( QString key ) +{ + if(key == "default") + key = "infinity"; + + auto iter = d->index.find(key); + if(iter != d->index.end()) + return *iter; + + + return -1; +} + + +void IconList::drop() +{ + mutex.lock(); + delete m_Instance; + m_Instance = 0; + mutex.unlock(); +} + +IconList* IconList::instance() +{ + if ( !m_Instance ) + { + mutex.lock(); + if ( !m_Instance ) + m_Instance = new IconList; + mutex.unlock(); + } + return m_Instance; +} + +#include "IconListModel.moc" \ No newline at end of file diff --git a/logic/IconListModel.h b/logic/IconListModel.h new file mode 100644 index 00000000..31b05e64 --- /dev/null +++ b/logic/IconListModel.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +class Private; + +class IconList : public QAbstractListModel +{ +public: + static IconList* instance(); + static void drop(); + QIcon getIcon ( QString key ); + int getIconIndex ( QString key ); + + virtual QVariant data ( const QModelIndex& index, int role = Qt::DisplayRole ) const; + virtual int rowCount ( const QModelIndex& parent = QModelIndex() ) const; + + bool addIcon(QString key, QString name, QString path, bool is_builtin = false); + + +private: + virtual ~IconList(); + IconList(); + // hide copy constructor + IconList ( const IconList & ) = delete; + // hide assign op + IconList& operator= ( const IconList & ) = delete; + static IconList* m_Instance; + static QMutex mutex; + Private* d; +}; diff --git a/logic/InstanceFactory.cpp b/logic/InstanceFactory.cpp new file mode 100644 index 00000000..f3511157 --- /dev/null +++ b/logic/InstanceFactory.cpp @@ -0,0 +1,113 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceFactory.h" + +#include +#include + +#include "BaseInstance.h" +#include "LegacyInstance.h" +#include "OneSixInstance.h" +#include "NostalgiaInstance.h" +#include "InstanceVersion.h" +#include "MinecraftVersion.h" + +#include "inifile.h" +#include +#include + +#include "pathutils.h" + +InstanceFactory InstanceFactory::loader; + +InstanceFactory::InstanceFactory() : + QObject(NULL) +{ + +} + +InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst, const QString &instDir) +{ + auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg")); + + m_settings->registerSetting(new Setting("InstanceType", "Legacy")); + + QString inst_type = m_settings->get("InstanceType").toString(); + + //FIXME: replace with a map lookup, where instance classes register their types + if(inst_type == "Legacy") + { + inst = new LegacyInstance(instDir, m_settings, this); + } + else if(inst_type == "OneSix") + { + inst = new OneSixInstance(instDir, m_settings, this); + } + else if(inst_type == "Nostalgia") + { + inst = new NostalgiaInstance(instDir, m_settings, this); + } + else + { + return InstanceFactory::UnknownLoadError; + } + return NoLoadError; +} + + +InstanceFactory::InstCreateError InstanceFactory::createInstance( BaseInstance*& inst, InstVersionPtr version, const QString& instDir ) +{ + QDir rootDir(instDir); + + qDebug(instDir.toUtf8()); + if (!rootDir.exists() && !rootDir.mkpath(".")) + { + return InstanceFactory::CantCreateDir; + } + auto mcVer = version.dynamicCast(); + if(!mcVer) + return InstanceFactory::NoSuchVersion; + + auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg")); + m_settings->registerSetting(new Setting("InstanceType", "Legacy")); + + switch(mcVer->type) + { + case MinecraftVersion::Legacy: + m_settings->set("InstanceType", "Legacy"); + inst = new LegacyInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor); + break; + case MinecraftVersion::OneSix: + m_settings->set("InstanceType", "OneSix"); + inst = new OneSixInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor); + break; + case MinecraftVersion::Nostalgia: + m_settings->set("InstanceType", "Nostalgia"); + inst = new NostalgiaInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor); + break; + default: + { + delete m_settings; + return InstanceFactory::NoSuchVersion; + } + } + + //FIXME: really, how do you even know? + return InstanceFactory::NoCreateError; +} diff --git a/logic/InstanceFactory.h b/logic/InstanceFactory.h new file mode 100644 index 00000000..ed54f520 --- /dev/null +++ b/logic/InstanceFactory.h @@ -0,0 +1,80 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "InstanceVersion.h" + +class InstVersion; +class BaseInstance; + +/*! + * The \bInstanceFactory\b is a singleton that manages loading and creating instances. + */ +class InstanceFactory : public QObject +{ + Q_OBJECT +public: + /*! + * \brief Gets a reference to the instance loader. + */ + static InstanceFactory &get() { return loader; } + + enum InstLoadError + { + NoLoadError = 0, + UnknownLoadError, + NotAnInstance + }; + + enum InstCreateError + { + NoCreateError = 0, + NoSuchVersion, + UnknownCreateError, + InstExists, + CantCreateDir + }; + + /*! + * \brief Creates a stub instance + * + * \param inst Pointer to store the created instance in. + * \param instDir The instance's directory. + * \return An InstCreateError error code. + * - InstExists if the given instance directory is already an instance. + * - CantCreateDir if the given instance directory cannot be created. + */ + InstCreateError createInstance(BaseInstance *&inst, InstVersionPtr version, const QString &instDir); + + /*! + * \brief Loads an instance from the given directory. + * Checks the instance's INI file to figure out what the instance's type is first. + * \param inst Pointer to store the loaded instance in. + * \param instDir The instance's directory. + * \return An InstLoadError error code. + * - NotAnInstance if the given instance directory isn't a valid instance. + */ + InstLoadError loadInstance(BaseInstance *&inst, const QString &instDir); + +private: + InstanceFactory(); + + static InstanceFactory loader; +}; diff --git a/logic/InstanceVersion.h b/logic/InstanceVersion.h new file mode 100644 index 00000000..eecd9c4e --- /dev/null +++ b/logic/InstanceVersion.h @@ -0,0 +1,68 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +/*! + * An abstract base class for versions. + */ +struct InstVersion +{ + /*! + * Checks if this version is less (older) than the given version. + * \param other The version to compare this one to. + * \return True if this version is older than the given version. + */ + virtual bool operator<(const InstVersion &rhs) const + { + return timestamp < rhs.timestamp; + } + + /*! + * Checks if this version is greater (newer) than the given version. + * \param other The version to compare this one to. + * \return True if this version is newer than the given version. + */ + virtual bool operator>( const InstVersion& rhs ) const + { + return timestamp > rhs.timestamp; + } + + /*! + * A string used to identify this version in config files. + * This should be unique within the version list or shenanigans will occur. + */ + QString descriptor; + /*! + * The name of this version as it is displayed to the user. + * For example: "1.5.1" + */ + QString name; + /*! + * Gets the version's timestamp. + * This is primarily used for sorting versions in a list. + */ + qint64 timestamp; + + virtual QString typeString() const + { + return "InstVersion"; + } +}; + +typedef QSharedPointer InstVersionPtr; + +Q_DECLARE_METATYPE( InstVersionPtr ) \ No newline at end of file diff --git a/logic/LegacyForge.cpp b/logic/LegacyForge.cpp new file mode 100644 index 00000000..adcf487c --- /dev/null +++ b/logic/LegacyForge.cpp @@ -0,0 +1,57 @@ +// +// Copyright 2012 MultiMC Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "LegacyForge.h" + +MinecraftForge::MinecraftForge ( const QString& file ) : Mod ( file ) +{ + +} +bool MinecraftForge::FixVersionIfNeeded ( QString newVersion ) +{/* + wxString reportedVersion = GetModVersion(); + if(reportedVersion == "..." || reportedVersion.empty()) + { + std::auto_ptr in(new wxFFileInputStream("forge.zip")); + wxTempFileOutputStream out("forge.zip"); + wxTextOutputStream textout(out); + wxZipInputStream inzip(*in); + wxZipOutputStream outzip(out); + std::auto_ptr entry; + // preserve metadata + outzip.CopyArchiveMetaData(inzip); + // copy all entries + while (entry.reset(inzip.GetNextEntry()), entry.get() != NULL) + if (!outzip.CopyEntry(entry.release(), inzip)) + return false; + // release last entry + in.reset(); + outzip.PutNextEntry("forgeversion.properties"); + + wxStringTokenizer tokenizer(newVersion,"."); + wxString verFile; + verFile << wxString("forge.major.number=") << tokenizer.GetNextToken() << "\n"; + verFile << wxString("forge.minor.number=") << tokenizer.GetNextToken() << "\n"; + verFile << wxString("forge.revision.number=") << tokenizer.GetNextToken() << "\n"; + verFile << wxString("forge.build.number=") << tokenizer.GetNextToken() << "\n"; + auto buf = verFile.ToUTF8(); + outzip.Write(buf.data(), buf.length()); + // check if we succeeded + return inzip.Eof() && outzip.Close() && out.Commit(); + } + */ + return true; +} diff --git a/logic/LegacyForge.h b/logic/LegacyForge.h new file mode 100644 index 00000000..00a054b8 --- /dev/null +++ b/logic/LegacyForge.h @@ -0,0 +1,25 @@ +// +// Copyright 2012 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 "Mod.h" + +class MinecraftForge : public Mod +{ +public: + MinecraftForge ( const QString& file ); + bool FixVersionIfNeeded(QString newVersion); +}; diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp new file mode 100644 index 00000000..db2a72d9 --- /dev/null +++ b/logic/LegacyInstance.cpp @@ -0,0 +1,274 @@ +#include "LegacyInstance.h" +#include "LegacyInstance_p.h" +#include "MinecraftProcess.h" +#include "LegacyUpdate.h" +#include +#include +#include +#include "gui/LegacyModEditDialog.h" +#include +#include +#include + +#define LAUNCHER_FILE "MultiMCLauncher.jar" + +LegacyInstance::LegacyInstance(const QString& rootDir, SettingsObject* settings, QObject* parent) + :BaseInstance( new LegacyInstancePrivate(),rootDir, settings, parent) +{ + settings->registerSetting(new Setting("NeedsRebuild", true)); + settings->registerSetting(new Setting("ShouldUpdate", false)); + settings->registerSetting(new Setting("JarVersion", "Unknown")); + settings->registerSetting(new Setting("LwjglVersion", "2.9.0")); + settings->registerSetting(new Setting("IntendedJarVersion", "")); +} + +BaseUpdate* LegacyInstance::doUpdate() +{ + return new LegacyUpdate(this, this); +} + +MinecraftProcess* LegacyInstance::prepareForLaunch(QString user, QString session) +{ + MinecraftProcess * proc = new MinecraftProcess(this); + + // FIXME: extract the icon + // QImage(":/icons/instances/" + iconKey()).save(PathCombine(minecraftRoot(), "icon.png")); + + // extract the legacy launcher + QFile(":/launcher/launcher.jar").copy(PathCombine(minecraftRoot(), LAUNCHER_FILE)); + + // set the process arguments + { + QStringList args; + + // window size + QString windowSize; + if (settings().get("LaunchMaximized").toBool()) + windowSize = "max"; + else + windowSize = QString("%1x%2"). + arg(settings().get("MinecraftWinWidth").toInt()). + arg(settings().get("MinecraftWinHeight").toInt()); + + // window title + QString windowTitle; + windowTitle.append("MultiMC: ").append(name()); + + // Java arguments + args.append(Util::Commandline::splitArgs(settings().get("JvmArgs").toString())); + +#ifdef OSX + // OSX dock icon and name + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(windowTitle); +#endif + + QString lwjgl = QDir(globalSettings->get("LWJGLDir").toString() + "/" + lwjglVersion()).absolutePath(); + + // launcher arguments + args << QString("-Xms%1m").arg(settings().get("MinMemAlloc").toInt()); + args << QString("-Xmx%1m").arg(settings().get("MaxMemAlloc").toInt()); + args << "-jar" << LAUNCHER_FILE; + args << user; + args << session; + args << windowTitle; + args << windowSize; + args << lwjgl; + proc->setMinecraftArguments(args); + } + + // set the process work path + proc->setMinecraftWorkdir(minecraftRoot()); + + return proc; +} + +QSharedPointer< ModList > LegacyInstance::coreModList() +{ + I_D(LegacyInstance); + if(!d->core_mod_list) + { + d->core_mod_list.reset(new ModList(coreModsDir(), QString())); + } + return d->core_mod_list; +} + +QSharedPointer< ModList > LegacyInstance::jarModList() +{ + I_D(LegacyInstance); + if(!d->jar_mod_list) + { + auto list = new ModList(instModsDir(), modListFile()); + connect(list, SIGNAL(changed()), SLOT(jarModsChanged())); + d->jar_mod_list.reset(list); + } + return d->jar_mod_list; +} + +void LegacyInstance::jarModsChanged() +{ + setShouldRebuild(true); +} + + +QSharedPointer< ModList > LegacyInstance::loaderModList() +{ + I_D(LegacyInstance); + if(!d->loader_mod_list) + { + d->loader_mod_list.reset(new ModList(mlModsDir(), QString())); + } + return d->loader_mod_list; +} + +QSharedPointer< QDialog > LegacyInstance::createModEditDialog ( QWidget* parent ) +{ + return QSharedPointer (new LegacyModEditDialog(this, parent)); +} + + +void LegacyInstance::cleanupAfterRun() +{ + //FIXME: delete the launcher and icons and whatnot. +} + + +QString LegacyInstance::instModsDir() const +{ + return PathCombine(instanceRoot(), "instMods"); +} + +QString LegacyInstance::binDir() const +{ + return PathCombine(minecraftRoot(), "bin"); +} + +QString LegacyInstance::savesDir() const +{ + return PathCombine(minecraftRoot(), "saves"); +} + +QString LegacyInstance::mlModsDir() const +{ + return PathCombine(minecraftRoot(), "mods"); +} + +QString LegacyInstance::coreModsDir() const +{ + return PathCombine(minecraftRoot(), "coremods"); +} + +QString LegacyInstance::resourceDir() const +{ + return PathCombine(minecraftRoot(), "resources"); +} + +QString LegacyInstance::mcJar() const +{ + return PathCombine(binDir(), "minecraft.jar"); +} + +QString LegacyInstance::mcBackup() const +{ + return PathCombine(binDir(), "mcbackup.jar"); +} + +QString LegacyInstance::modListFile() const +{ + return PathCombine(instanceRoot(), "modlist"); +} + +bool LegacyInstance::shouldUpdateCurrentVersion() const +{ + QFileInfo jar(mcJar()); + return jar.lastModified().toUTC().toMSecsSinceEpoch() != lastCurrentVersionUpdate(); +} + +void LegacyInstance::updateCurrentVersion(bool keepCurrent) +{ + QFileInfo jar(mcJar()); + + if(!jar.exists()) + { + setLastCurrentVersionUpdate(0); + setCurrentVersionId("Unknown"); + return; + } + + qint64 time = jar.lastModified().toUTC().toMSecsSinceEpoch(); + + setLastCurrentVersionUpdate(time); + if (!keepCurrent) + { + // TODO: Implement GetMinecraftJarVersion function. + QString newVersion = "Unknown";//javautils::GetMinecraftJarVersion(jar.absoluteFilePath()); + setCurrentVersionId(newVersion); + } +} +qint64 LegacyInstance::lastCurrentVersionUpdate() const +{ + I_D(LegacyInstance); + return d->m_settings->get ( "lastVersionUpdate" ).value(); +} +void LegacyInstance::setLastCurrentVersionUpdate ( qint64 val ) +{ + I_D(LegacyInstance); + d->m_settings->set ( "lastVersionUpdate", val ); +} +bool LegacyInstance::shouldRebuild() const +{ + I_D(LegacyInstance); + return d->m_settings->get ( "NeedsRebuild" ).toBool(); +} +void LegacyInstance::setShouldRebuild ( bool val ) +{ + I_D(LegacyInstance); + d->m_settings->set ( "NeedsRebuild", val ); +} +QString LegacyInstance::currentVersionId() const +{ + I_D(LegacyInstance); + return d->m_settings->get ( "JarVersion" ).toString(); +} + +void LegacyInstance::setCurrentVersionId ( QString val ) +{ + I_D(LegacyInstance); + d->m_settings->set ( "JarVersion", val ); +} + +QString LegacyInstance::lwjglVersion() const +{ + I_D(LegacyInstance); + return d->m_settings->get ( "LwjglVersion" ).toString(); +} +void LegacyInstance::setLWJGLVersion ( QString val ) +{ + I_D(LegacyInstance); + d->m_settings->set ( "LwjglVersion", val ); +} +QString LegacyInstance::intendedVersionId() const +{ + I_D(LegacyInstance); + return d->m_settings->get ( "IntendedJarVersion" ).toString(); +} +bool LegacyInstance::setIntendedVersionId ( QString version ) +{ + settings().set("IntendedJarVersion", version); + setShouldUpdate(true); + return true; +} +bool LegacyInstance::shouldUpdate() const +{ + I_D(LegacyInstance); + 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 ); +} diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h new file mode 100644 index 00000000..43a66a2b --- /dev/null +++ b/logic/LegacyInstance.h @@ -0,0 +1,95 @@ +#pragma once + +#include "BaseInstance.h" + +class ModList; +class BaseUpdate; + +class LegacyInstance : public BaseInstance +{ + Q_OBJECT +public: + + explicit LegacyInstance(const QString &rootDir, SettingsObject * settings, QObject *parent = 0); + + /// Path to the instance's minecraft.jar + QString mcJar() const; + + //! Path to the instance's mcbackup.jar + QString mcBackup() const; + + //! Path to the instance's modlist file. + QString modListFile() const; + + ////// Mod Lists ////// + QSharedPointer jarModList(); + QSharedPointer coreModList(); + QSharedPointer loaderModList(); + + ////// Directories ////// + QString savesDir() const; + QString instModsDir() const; + QString binDir() const; + QString mlModsDir() const; + QString coreModsDir() const; + QString resourceDir() const; + + /*! + * \brief Checks whether or not the currentVersion of the instance needs to be updated. + * If this returns true, updateCurrentVersion is called. In the + * standard instance, this is determined by checking a timestamp + * stored in the instance config file against the last modified time of Minecraft.jar. + * \return True if updateCurrentVersion() should be called. + */ + bool shouldUpdateCurrentVersion() const; + + /*! + * \brief Updates the current version. + * This function should first set the current version timestamp + * (setCurrentVersionTimestamp()) to the current time. Next, if + * keepCurrent is false, this function should check what the + * instance's current version is and call setCurrentVersion() to + * update it. This function will automatically be called when the + * instance is loaded if shouldUpdateCurrentVersion returns true. + * \param keepCurrent If true, only the version timestamp will be updated. + */ + void updateCurrentVersion(bool keepCurrent = false); + + /*! + * Gets the last time that the current version was checked. + * This is checked against the last modified time on the jar file to see if + * the current version needs to be checked again. + */ + qint64 lastCurrentVersionUpdate() const; + void setLastCurrentVersionUpdate(qint64 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; + virtual void setCurrentVersionId(QString val); + + //! The version of LWJGL that this instance uses. + QString lwjglVersion() const; + /// st the version of LWJGL libs this instance will use + void setLWJGLVersion(QString val); + + virtual QString intendedVersionId() const; + virtual bool setIntendedVersionId ( QString version ); + + virtual bool shouldUpdate() const; + virtual void setShouldUpdate(bool val); + virtual BaseUpdate* doUpdate(); + + virtual MinecraftProcess* prepareForLaunch( QString user, QString session ); + virtual void cleanupAfterRun(); + virtual QSharedPointer< QDialog > createModEditDialog ( QWidget* parent ); + +protected slots: + virtual void jarModsChanged(); +}; \ No newline at end of file diff --git a/logic/LegacyInstance_p.h b/logic/LegacyInstance_p.h new file mode 100644 index 00000000..a1d195b4 --- /dev/null +++ b/logic/LegacyInstance_p.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include +#include "BaseInstance_p.h" +#include "ModList.h" +#include + +class ModList; + +struct LegacyInstancePrivate: public BaseInstancePrivate +{ + QSharedPointer jar_mod_list; + QSharedPointer core_mod_list; + QSharedPointer loader_mod_list; +}; \ No newline at end of file diff --git a/logic/LegacyUpdate.cpp b/logic/LegacyUpdate.cpp new file mode 100644 index 00000000..a748bad3 --- /dev/null +++ b/logic/LegacyUpdate.cpp @@ -0,0 +1,381 @@ +#include "LegacyUpdate.h" +#include "lists/LwjglVersionList.h" +#include "lists/MinecraftVersionList.h" +#include "BaseInstance.h" +#include "LegacyInstance.h" +#include "net/NetWorker.h" +#include +#include +#include + + +LegacyUpdate::LegacyUpdate ( BaseInstance* inst, QObject* parent ) : BaseUpdate ( inst, parent ) {} + +void LegacyUpdate::executeTask() +{ + lwjglStart(); +} + +void LegacyUpdate::lwjglStart() +{ + LegacyInstance * inst = (LegacyInstance *) m_inst; + + lwjglVersion = inst->lwjglVersion(); + lwjglTargetPath = PathCombine("lwjgl", lwjglVersion ); + lwjglNativesPath = PathCombine( lwjglTargetPath, "natives/"); + + // if the 'done' file exists, we don't have to download this again + QFileInfo doneFile(PathCombine(lwjglTargetPath, "done")); + if(doneFile.exists()) + { + jarStart(); + return; + } + + auto &list = LWJGLVersionList::get(); + if(!list.isLoaded()) + { + emitFailed("Too soon! Let the LWJGL list load :)"); + return; + } + + setStatus("Downloading new LWJGL."); + auto version = list.getVersion(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 = NetWorker::spawn(); + QNetworkRequest req(realUrl); + req.setRawHeader("Host", hostname.toLatin1()); + req.setHeader(QNetworkRequest::UserAgentHeader, "Wget/1.14 (linux-gnu)"); + QNetworkReply * rep = worker.get ( req ); + + m_reply = QSharedPointer (rep, &QObject::deleteLater); + connect(rep, SIGNAL(downloadProgress(qint64,qint64)), SLOT(updateDownloadProgress(qint64,qint64))); + connect(&worker, SIGNAL(finished(QNetworkReply*)), SLOT(lwjglFinished(QNetworkReply*))); + //connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +} + +void LegacyUpdate::lwjglFinished(QNetworkReply* reply) +{ + if(m_reply != 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 = NetWorker::spawn(); + //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()) + { + auto &worker = NetWorker::spawn(); + 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, "Wget/1.14 (linux-gnu)"); + QNetworkReply * rep = worker.get(req); + connect(rep, SIGNAL(downloadProgress(qint64,qint64)), SLOT(updateDownloadProgress(qint64,qint64))); + m_reply = QSharedPointer (rep, &QObject::deleteLater); + return; + } + QFile saveMe("lwjgl.zip"); + saveMe.open(QIODevice::WriteOnly); + saveMe.write(m_reply->readAll()); + saveMe.close(); + setStatus("Installing new LWJGL..."); + extractLwjgl(); + jarStart(); +} +void LegacyUpdate::extractLwjgl() +{ + // make sure the directories are there + + bool success = ensurePathExists(lwjglNativesPath); + + if(!success) + { + emitFailed("Failed to extract the lwjgl libs - error when creating required folders."); + return; + } + + QuaZip zip("lwjgl.zip"); + if(!zip.open(QuaZip::mdUnzip)) + { + emitFailed("Failed to extract the lwjgl libs - not a valid archive."); + return; + } + + // and now we are going to access files inside it + QuaZipFile file(&zip); + const QString jarNames[] = { "jinput.jar", "lwjgl_util.jar", "lwjgl.jar" }; + for(bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) + { + if(!file.open(QIODevice::ReadOnly)) + { + zip.close(); + emitFailed("Failed to extract the lwjgl libs - error while reading archive."); + return; + } + QuaZipFileInfo info; + QString name = file.getActualFileName(); + if(name.endsWith('/')) + { + file.close(); + continue; + } + QString destFileName; + // Look for the jars + for (int i = 0; i < 3; i++) + { + if (name.endsWith(jarNames[i])) + { + destFileName = PathCombine(lwjglTargetPath, jarNames[i]); + } + } + // Not found? look for the natives + if(destFileName.isEmpty()) + { +#ifdef Q_OS_WIN32 + QString nativesDir = "windows"; +#elif Q_OS_MAC + QString nativesDir = "macosx"; +#else + QString nativesDir = "linux"; +#endif + if (name.contains(nativesDir)) + { + int lastSlash = name.lastIndexOf('/'); + int lastBackSlash = name.lastIndexOf('/'); + if(lastSlash != -1) + name = name.mid(lastSlash+1); + else if(lastBackSlash != -1) + name = name.mid(lastBackSlash+1); + destFileName = PathCombine(lwjglNativesPath, name); + } + } + // Now if destFileName is still empty, go to the next file. + if (!destFileName.isEmpty()) + { + setStatus("Installing new LWJGL - Extracting " + name); + QFile output(destFileName); + output.open(QIODevice::WriteOnly); + output.write(file.readAll()); // FIXME: wste of memory!? + output.close(); + } + file.close(); // do not forget to close! + } + zip.close(); + m_reply.clear(); + QFile doneFile(PathCombine(lwjglTargetPath, "done")); + doneFile.open(QIODevice::WriteOnly); + doneFile.write("done."); + doneFile.close(); +} + +void LegacyUpdate::lwjglFailed() +{ + emitFailed("Bad stuff happened while trying to get the lwjgl libs..."); +} + +void LegacyUpdate::jarStart() +{ + setStatus("Checking ..."); + LegacyInstance * inst = (LegacyInstance *) m_inst; + QString current_version_id = inst->currentVersionId(); + QString intended_version_id = inst->intendedVersionId(); + bool shouldUpdate = inst->shouldUpdate(); + if(!shouldUpdate) + { + emitSucceeded(); + return; + } + + // nuke the backup file, we are replacing the base jar anyway + QFile mc_backup(inst->mcBackup()); + if (mc_backup.exists()) + { + mc_backup.remove(); + } + + // Get a pointer to the version object that corresponds to the instance's version. + auto targetVersion = MinecraftVersionList::getMainList().findVersion(intended_version_id); + + if(!targetVersion) + { + emitFailed("Not a valid version:" + intended_version_id); + return; + } + + // 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("Downloading new minecraft.jar"); + + // This will be either 'minecraft' or the version number, depending on where + // we're downloading from. + QString jarFilename = "minecraft"; + QString download_path = PathCombine(inst->minecraftRoot(), "bin/minecraft.jar"); + + QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/"); + urlstr += targetVersion->descriptor + "/" + targetVersion->descriptor + ".jar"; + auto dljob = DownloadJob::create(QUrl(urlstr), download_path); + + legacyDownloadJob.reset(new JobList()); + legacyDownloadJob->add(dljob); + connect(legacyDownloadJob.data(), SIGNAL(finished()), SLOT(jarFinished())); + connect(legacyDownloadJob.data(), SIGNAL(failed()), SLOT(jarFailed())); + connect(legacyDownloadJob.data(), SIGNAL(progress(qint64,qint64)), SLOT(updateDownloadProgress(qint64,qint64))); + download_queue.enqueue(legacyDownloadJob); +} + +void LegacyUpdate::jarFinished() +{ + // process the jar + emitSucceeded(); +} + +void LegacyUpdate::jarFailed() +{ + // bad, bad + emitFailed("Failed to download the minecraft jar. Try again later."); +} + +void LegacyUpdate::ModTheJar() +{ + /* + LegacyInstance * inst = (LegacyInstance *) m_inst; + // Get the mod list + auto modList = inst->getJarModList(); + + QFileInfo mcBin(inst->binDir()); + QFileInfo mcJar(inst->mcJar()); + QFileInfo mcBackup(inst->mcBackup()); + + // Nothing to do if there are no jar mods to install, no backup and just the mc jar + if(mcJar.isFile() && !mcBackup.exists() && modList->empty()) + { + inst->setShouldRebuild(false); + emitSucceeded(); + return; + } + + setStatus("Installing mods - backing up minecraft.jar..."); + if (!mcBackup.exists() && !QFile::copy(mcJar.absoluteFilePath(), mcBackup.absoluteFilePath()) ) + { + emitFailed("Failed to back up minecraft.jar"); + return; + } + + if (mcJar.isFile() && !QFile::remove(mcJar.absoluteFilePath())) + { + emitFailed("Failed to delete old minecraft.jar"); + return; + } + + setStatus("Installing mods - Opening minecraft.jar"); + + wxFFileOutputStream jarStream(mcJar.absoluteFilePath()); + wxZipOutputStream zipOut(jarStream); + + // Files already added to the jar. + // These files will be skipped. + QSet addedFiles; + + // Modify the jar + setStatus("Installing mods - Adding mod files..."); + for (ModList::const_reverse_iterator iter = modList->rbegin(); iter != modList->rend(); iter++) + { + wxFileName modFileName = iter->GetFileName(); + setStatus("Installing mods - Adding " + modFileName.GetFullName()); + if (iter->GetModType() == Mod::ModType::MOD_ZIPFILE) + { + wxFFileInputStream modStream(modFileName.GetFullPath()); + wxZipInputStream zipStream(modStream); + std::unique_ptr entry; + while (entry.reset(zipStream.GetNextEntry()), entry.get() != NULL) + { + if (entry->IsDir()) + continue; + + wxString name = entry->GetName(); + if (addedFiles.count(name) == 0) + { + if (!zipOut.CopyEntry(entry.release(), zipStream)) + break; + addedFiles.insert(name); + } + } + } + else + { + wxFileName destFileName = modFileName; + destFileName.MakeRelativeTo(m_inst->GetInstModsDir().GetFullPath()); + wxString destFile = destFileName.GetFullPath(); + + if (addedFiles.count(destFile) == 0) + { + wxFFileInputStream input(modFileName.GetFullPath()); + zipOut.PutNextEntry(destFile); + zipOut.Write(input); + + addedFiles.insert(destFile); + } + } + } + + { + wxFFileInputStream inStream(mcBackup.GetFullPath()); + wxZipInputStream zipIn(inStream); + + std::auto_ptr entry; + while (entry.reset(zipIn.GetNextEntry()), entry.get() != NULL) + { + wxString name = entry->GetName(); + + if (!name.Matches("META-INF*") && + addedFiles.count(name) == 0) + { + if (!zipOut.CopyEntry(entry.release(), zipIn)) + break; + addedFiles.insert(name); + } + } + } + + // Recompress the jar + TaskStep(); // STEP 3 + SetStatus(_("Installing mods - Recompressing jar...")); + + inst->SetNeedsRebuild(false); + inst->UpdateVersion(true); + return (ExitCode)1; + */ +} \ No newline at end of file diff --git a/logic/LegacyUpdate.h b/logic/LegacyUpdate.h new file mode 100644 index 00000000..342d1eab --- /dev/null +++ b/logic/LegacyUpdate.h @@ -0,0 +1,67 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "net/DownloadJob.h" +#include "tasks/Task.h" +#include "BaseUpdate.h" + +class MinecraftVersion; +class BaseInstance; + +class LegacyUpdate : public BaseUpdate +{ + Q_OBJECT +public: + explicit LegacyUpdate(BaseInstance *inst, QObject *parent = 0); + virtual void executeTask(); + +private slots: + void lwjglStart(); + void lwjglFinished( QNetworkReply* ); + void lwjglFailed(); + + void jarStart(); + void jarFinished(); + void jarFailed(); + + void extractLwjgl(); + + void ModTheJar(); +private: + + QSharedPointer m_reply; + + // target version, determined during this task + // MinecraftVersion *targetVersion; + QString lwjglURL; + QString lwjglVersion; + + QString lwjglTargetPath; + QString lwjglNativesPath; +private: + JobListPtr legacyDownloadJob; + JobListQueue download_queue; + + // target version, determined during this task + QSharedPointer targetVersion; +}; + + diff --git a/logic/MinecraftProcess.cpp b/logic/MinecraftProcess.cpp new file mode 100644 index 00000000..d34be835 --- /dev/null +++ b/logic/MinecraftProcess.cpp @@ -0,0 +1,169 @@ +/* Copyright 2013 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 "MinecraftProcess.h" + +#include +#include +#include +//#include +#include + +#include "BaseInstance.h" + +#include "osutils.h" +#include "pathutils.h" +#include "cmdutils.h" + +#define IBUS "@im=ibus" + +// constructor +MinecraftProcess::MinecraftProcess( BaseInstance* inst ) : + m_instance(inst) +{ + connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(finish(int, QProcess::ExitStatus))); + + // prepare the process environment + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + +#ifdef LINUX + // Strip IBus + if (env.value("XMODIFIERS").contains(IBUS)) + env.insert("XMODIFIERS", env.value("XMODIFIERS").replace(IBUS, "")); +#endif + + // export some infos + env.insert("INST_NAME", inst->name()); + env.insert("INST_ID", inst->id()); + env.insert("INST_DIR", QDir(inst->instanceRoot()).absolutePath()); + + this->setProcessEnvironment(env); + m_prepostlaunchprocess.setProcessEnvironment(env); + + // std channels + connect(this, SIGNAL(readyReadStandardError()), SLOT(on_stdErr())); + connect(this, SIGNAL(readyReadStandardOutput()), SLOT(on_stdOut())); +} + +void MinecraftProcess::setMinecraftArguments ( QStringList args ) +{ + m_args = args; +} + +void MinecraftProcess::setMinecraftWorkdir ( QString path ) +{ + QDir mcDir(path); + this->setWorkingDirectory(mcDir.absolutePath()); + m_prepostlaunchprocess.setWorkingDirectory(mcDir.absolutePath()); +} + + +// console window +void MinecraftProcess::on_stdErr() +{ + QByteArray data = readAllStandardError(); + QString str = m_err_leftover + QString::fromLocal8Bit(data); + m_err_leftover.clear(); + QStringList lines = str.split("\n"); + bool complete = str.endsWith("\n"); + + for(int i = 0; i < lines.size() - 1; i++) + { + QString & line = lines[i]; + MessageLevel::Enum level = MessageLevel::Error; + if(line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || line.contains("[FINEST]") ) + level = MessageLevel::Message; + if(line.contains("[SEVERE]") || line.contains("[WARNING]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + emit log(lines[i].toLocal8Bit(), level); + } + if(!complete) + m_err_leftover = lines.last(); +} + +void MinecraftProcess::on_stdOut() +{ + QByteArray data = readAllStandardOutput(); + QString str = m_out_leftover + QString::fromLocal8Bit(data); + m_out_leftover.clear(); + QStringList lines = str.split("\n"); + bool complete = str.endsWith("\n"); + + for(int i = 0; i < lines.size() - 1; i++) + { + QString & line = lines[i]; + emit log(lines[i].toLocal8Bit(), MessageLevel::Message); + } + if(!complete) + m_out_leftover = lines.last(); +} + +// exit handler +void MinecraftProcess::finish(int code, ExitStatus status) +{ + if (status != NormalExit) + { + //TODO: error handling + } + + emit log("Minecraft exited."); + + m_prepostlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(code)); + + // run post-exit + if (!m_instance->settings().get("PostExitCommand").toString().isEmpty()) + { + m_prepostlaunchprocess.start(m_instance->settings().get("PostExitCommand").toString()); + m_prepostlaunchprocess.waitForFinished(); + if (m_prepostlaunchprocess.exitStatus() != NormalExit) + { + //TODO: error handling + } + } + m_instance->cleanupAfterRun(); + emit ended(); +} + +void MinecraftProcess::launch() +{ + if (!m_instance->settings().get("PreLaunchCommand").toString().isEmpty()) + { + m_prepostlaunchprocess.start(m_instance->settings().get("PreLaunchCommand").toString()); + m_prepostlaunchprocess.waitForFinished(); + if (m_prepostlaunchprocess.exitStatus() != NormalExit) + { + //TODO: error handling + return; + } + } + + m_instance->setLastLaunch(); + + emit log(QString("Minecraft folder is: '%1'").arg(workingDirectory())); + QString JavaPath = m_instance->settings().get("JavaPath").toString(); + emit log(QString("Java path: '%1'").arg(JavaPath)); + emit log(QString("Arguments: '%1'").arg(m_args.join("' '"))); + start(JavaPath, m_args); + if (!waitForStarted()) + { + emit log("Could not launch minecraft!"); + return; + //TODO: error handling + } +} + + diff --git a/logic/MinecraftProcess.h b/logic/MinecraftProcess.h new file mode 100644 index 00000000..516bf986 --- /dev/null +++ b/logic/MinecraftProcess.h @@ -0,0 +1,86 @@ +/* Copyright 2013 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. + */ +#pragma once + +#include + +#include "BaseInstance.h" + +/** + * @brief the MessageLevel Enum + * defines what level a message is + */ +namespace MessageLevel { +enum Enum { + MultiMC, /**< MultiMC Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal /**< Fatal Errors */ +}; +} + +/** + * @file data/minecraftprocess.h + * @brief The MinecraftProcess class + */ +class MinecraftProcess : public QProcess +{ + Q_OBJECT +public: + /** + * @brief MinecraftProcess constructor + * @param inst the Instance pointer to launch + */ + MinecraftProcess(BaseInstance *inst); + + /** + * @brief launch minecraft + */ + void launch(); + + void setMinecraftWorkdir(QString path); + + void setMinecraftArguments(QStringList args); + +signals: + /** + * @brief emitted when mc has finished and the PostLaunchCommand was run + */ + void ended(); + + /** + * @brief emitted when we want to log something + * @param text the text to log + * @param level the level to log at + */ + void log(QString text, MessageLevel::Enum level=MessageLevel::MultiMC); + +protected: + BaseInstance *m_instance; + QStringList m_args; + QString m_err_leftover; + QString m_out_leftover; + QProcess m_prepostlaunchprocess; + +protected slots: + void finish(int, QProcess::ExitStatus status); + void on_stdErr(); + void on_stdOut(); +}; diff --git a/logic/MinecraftVersion.h b/logic/MinecraftVersion.h new file mode 100644 index 00000000..27977262 --- /dev/null +++ b/logic/MinecraftVersion.h @@ -0,0 +1,76 @@ +/* Copyright 2013 Andrew Okin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "InstanceVersion.h" +#include + +struct MinecraftVersion : public InstVersion +{ + // From InstVersion: + /* + QString m_descriptor; + QString m_name; + qint64 m_timestamp; + */ + + /// The URL that this version will be downloaded from. maybe. + QString download_url; + + /// This version's type. Used internally to identify what kind of version this is. + enum VersionType + { + OneSix, + Legacy, + Nostalgia + } type; + + /// is this the latest version? + bool is_latest = false; + + /// is this a snapshot? + bool is_snapshot = false; + + virtual QString typeString() const + { + QStringList pre_final; + if(is_latest == true) + { + pre_final.append("Latest"); + } + switch (type) + { + case OneSix: + pre_final.append("OneSix"); + break; + case Legacy: + pre_final.append("Legacy"); + break; + case Nostalgia: + pre_final.append("Nostalgia"); + break; + + default: + pre_final.append(QString("Type(%1)").arg(type)); + break; + } + if(is_snapshot == true) + { + pre_final.append("Snapshot"); + } + return pre_final.join(' '); + } +}; diff --git a/logic/Mod.cpp b/logic/Mod.cpp new file mode 100644 index 00000000..652bbda7 --- /dev/null +++ b/logic/Mod.cpp @@ -0,0 +1,264 @@ +// +// Copyright 2012 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 "Mod.h" +#include +#include + +Mod::Mod( const QFileInfo& file ) +{ + repath(file); +} + +void Mod::repath ( const QFileInfo& file ) +{ + m_file = file; + m_name = file.baseName(); + m_id = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + if (m_file.isDir()) + m_type = MOD_FOLDER; + else if (m_file.isFile()) + { + QString ext = m_file.suffix().toLower(); + if (ext == "zip" || ext == "jar") + m_type = MOD_ZIPFILE; + else + m_type = MOD_SINGLEFILE; + } + + /* + switch (modType) + { + case MOD_ZIPFILE: + { + wxFFileInputStream fileIn(modFile.GetFullPath()); + wxZipInputStream zipIn(fileIn); + + std::auto_ptr entry; + + bool is_forge = false; + while(true) + { + entry.reset(zipIn.GetNextEntry()); + if (entry.get() == nullptr) + break; + if(entry->GetInternalName().EndsWith("mcmod.info")) + break; + if(entry->GetInternalName().EndsWith("forgeversion.properties")) + { + is_forge = true; + break; + } + } + + if (entry.get() != nullptr) + { + // Read the info file into text + wxString infoFileData; + wxStringOutputStream stringOut(&infoFileData); + zipIn.Read(stringOut); + if(!is_forge) + ReadModInfoData(infoFileData); + else + ReadForgeInfoData(infoFileData); + } + } + break; + + case MOD_FOLDER: + { + wxString infoFile = Path::Combine(modFile, "mcmod.info"); + if (!wxFileExists(infoFile)) + { + infoFile = wxEmptyString; + + wxDir modDir(modFile.GetFullPath()); + + if (!modDir.IsOpened()) + { + wxLogError(_("Can't fine mod info file. Failed to open mod folder.")); + break; + } + + wxString currentFile; + if (modDir.GetFirst(¤tFile)) + { + do + { + if (currentFile.EndsWith("mcmod.info")) + { + infoFile = Path::Combine(modFile.GetFullPath(), currentFile); + break; + } + } while (modDir.GetNext(¤tFile)); + } + } + + if (infoFile != wxEmptyString && wxFileExists(infoFile)) + { + wxString infoStr; + wxFFileInputStream fileIn(infoFile); + wxStringOutputStream strOut(&infoStr); + fileIn.Read(strOut); + ReadModInfoData(infoStr); + } + } + break; + } +*/ +} + + +/* +void ReadModInfoData(QString info) +{ + using namespace boost::property_tree; + + // Read the data + ptree ptRoot; + + std::stringstream stringIn(cStr(info)); + try + { + read_json(stringIn, ptRoot); + + ptree pt = ptRoot.get_child(ptRoot.count("modlist") == 1 ? "modlist" : "").begin()->second; + + modID = wxStr(pt.get("modid")); + modName = wxStr(pt.get("name")); + modVersion = wxStr(pt.get("version")); + } + catch (json_parser_error e) + { + // Silently fail... + } + catch (ptree_error e) + { + // Silently fail... + } +} +*/ + +// FIXME: abstraction violated. +/* +void Mod::ReadForgeInfoData(QString infoFileData) +{ + using namespace boost::property_tree; + + // Read the data + ptree ptRoot; + modName = "Minecraft Forge"; + modID = "Forge"; + std::stringstream stringIn(cStr(infoFileData)); + try + { + read_ini(stringIn, ptRoot); + wxString major, minor, revision, build; + // BUG: boost property tree is bad. won't let us get a key with dots in it + // Likely cause = treating the dots as path separators. + for (auto iter = ptRoot.begin(); iter != ptRoot.end(); iter++) + { + auto &item = *iter; + std::string key = item.first; + std::string value = item.second.get_value(); + if(key == "forge.major.number") + major = value; + if(key == "forge.minor.number") + minor = value; + if(key == "forge.revision.number") + revision = value; + if(key == "forge.build.number") + build = value; + } + modVersion.Empty(); + modVersion << major << "." << minor << "." << revision << "." << build; + } + catch (json_parser_error e) + { + std::cerr << e.what(); + } + catch (ptree_error e) + { + std::cerr << e.what(); + } +} +*/ + +bool Mod::replace ( Mod& with ) +{ + if(!destroy()) + return false; + bool success = false; + auto t = with.type(); + if(t == MOD_ZIPFILE || t == MOD_SINGLEFILE) + { + success = QFile::copy(with.m_file.filePath(), m_file.path()); + } + if(t == MOD_FOLDER) + { + success = copyPath(with.m_file.filePath(), m_file.path()); + } + if(success) + { + m_id = with.m_id; + m_mcversion = with.m_mcversion; + m_type = with.m_type; + m_name = with.m_name; + m_version = with.m_version; + } + return success; +} + +bool Mod::destroy() +{ + if(m_type == MOD_FOLDER) + { + QDir d(m_file.filePath()); + if(d.removeRecursively()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE) + { + QFile f(m_file.filePath()); + if(f.remove()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + return true; +} + + +QString Mod::version() const +{ + switch(type()) + { + case MOD_ZIPFILE: + return m_version; + case MOD_FOLDER: + return "Folder"; + case MOD_SINGLEFILE: + return "File"; + } +} diff --git a/logic/Mod.h b/logic/Mod.h new file mode 100644 index 00000000..f14818d1 --- /dev/null +++ b/logic/Mod.h @@ -0,0 +1,71 @@ +// +// Copyright 2012 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 + +class Mod +{ +public: + enum ModType + { + MOD_UNKNOWN, //!< Indicates an unspecified mod type. + MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. + MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). + MOD_FOLDER, //!< The mod is in a folder on the filesystem. + }; + + Mod(const QFileInfo &file); + + QFileInfo filename() const { return m_file; } + QString id() const { return m_id; } + ModType type() const { return m_type; } + QString mcversion() const { return m_mcversion; }; + bool valid() {return m_type != MOD_UNKNOWN;} + QString name() const {return m_name; }; + + QString version() const; + + + // delete all the files of this mod + bool destroy(); + // replace this mod with a copy of the other + bool replace(Mod & with); + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + + bool operator ==(const Mod &other) const + { + return filename() == other.filename(); + } + +protected: + + //FIXME: what do do with those? HMM... + /* + void ReadModInfoData(QString info); + void ReadForgeInfoData(QString infoFileData); + */ + + QFileInfo m_file; + QString m_id; + QString m_name; + QString m_version; + QString m_mcversion; + + ModType m_type; +}; diff --git a/logic/ModList.cpp b/logic/ModList.cpp new file mode 100644 index 00000000..d9e67574 --- /dev/null +++ b/logic/ModList.cpp @@ -0,0 +1,472 @@ +// +// Copyright 2013 MultiMC Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "ModList.h" +#include "LegacyInstance.h" +#include + +ModList::ModList ( const QString& dir, const QString& list_file ) +: QAbstractListModel(), m_dir(dir), m_list_file(list_file) +{ + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + m_dir.setSorting(QDir::Name); + update(); +} + +bool ModList::update() +{ + if (!isValid()) + return false; + + bool initial = mods.empty(); + + bool listChanged = false; + + auto list = m_dir.entryInfoList(); + for(auto entry: list) + { + Mod mod(entry); + if (initial || !mods.contains(mod)) + { + mods.push_back(mod); + listChanged = true; + } + } + return listChanged; +} + +bool ModList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool ModList::installMod ( const QFileInfo& filename, size_t index ) +{ + if(!filename.exists() || !filename.isReadable()) + { + return false; + } + Mod m(filename); + if(!m.valid()) + return false; + + // if it's already there, replace the original mod (in place) + int idx = mods.indexOf(m); + if(idx != -1) + { + if(mods[idx].replace(m)) + { + emit changed(); + return true; + } + return false; + } + + auto type = m.type(); + if(type == Mod::MOD_UNKNOWN) + return false; + if(type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE) + { + QString newpath = PathCombine(m_dir.path(), filename.fileName()); + if(!QFile::copy(filename.filePath(), newpath)) + return false; + m.repath(newpath); + mods.append(m); + emit changed(); + return true; + } + else if(type == Mod::MOD_FOLDER) + { + QString newpath = PathCombine(m_dir.path(), filename.fileName()); + if(!copyPath(filename.filePath(), newpath)) + return false; + m.repath(newpath); + mods.append(m); + emit changed(); + return true; + } + return false; +} + +bool ModList::deleteMod ( size_t index ) +{ + if(index >= mods.size()) + return false; + Mod & m = mods[index]; + if(m.destroy()) + { + mods.erase(mods.begin() + index); + emit changed(); + return true; + } + return false; +} + +bool ModList::moveMod ( size_t from, size_t to ) +{ + return false; +} + +int ModList::columnCount ( const QModelIndex& parent ) const +{ + return 2; +} + +QVariant ModList::data ( const QModelIndex& index, int role ) const +{ + if(!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if(row < 0 || row >= mods.size()) + return QVariant(); + + if(role != Qt::DisplayRole) + return QVariant(); + + switch(column) + { + case 0: + return mods[row].name(); + case 1: + return mods[row].version(); + case 2: + return mods[row].mcversion(); + default: + return QVariant(); + } +} + +QVariant ModList::headerData ( int section, Qt::Orientation orientation, int role ) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + switch (section) + { + case 0: + return QString("Mod Name"); + case 1: + return QString("Mod Version"); + case 2: + return QString("MC Version"); + } +} + + + +/* +ModList::ModList(const QString &dir) + : modsFolder(dir) +{ + +} + +bool ModList::update(bool quickLoad) +{ + bool listChanged = false; + + // Check for mods in the list whose files do not exist and remove them from the list. + // If doing a quickLoad, erase the whole list. + for (size_t i = 0; i < size(); i++) + { + if (quickLoad || !at(i).GetFileName().FileExists()) + { + erase(begin() + i); + i--; + listChanged = true; + } + } + + // Add any mods in the mods folder that aren't already in the list. + if (LoadModListFromDir(QString(), quickLoad)) + listChanged = true; + + return listChanged; +} + +bool ModList::LoadModListFromDir(const QString& loadFrom, bool quickLoad) +{ + QString dir(loadFrom.isEmpty() ? modsFolder : loadFrom); + + QDir modDir(dir); + if (!modDir.exists()) + return false; + + bool listChanged = false; + + auto list = modDir.entryInfoList(QDir::Readable|QDir::NoDotAndDotDot, QDir::Name); + for(auto currentFile: list) + { + if (currentFile.isFile()) + { + if (quickLoad || FindByFilename(currentFile.absoluteFilePath()) == nullptr) + { + Mod mod(currentFile.absoluteFilePath()); + push_back(mod); + listChanged = true; + } + } + else if (currentFile.isDir()) + { + if (LoadModListFromDir(currentFile.absoluteFilePath())) + listChanged = true; + } + } + + return listChanged; +} + +Mod *ModList::FindByFilename(const QString& filename) +{ + // Search the list for a mod with the given filename. + for (auto iter = begin(); iter != end(); ++iter) + { + if (iter->GetFileName() == QFileInfo(filename)) + return &(*iter); + } + + // If nothing is found, return nullptr. + return nullptr; +} + +int ModList::FindIndexByFilename(const QString& filename) +{ + // Search the list for a mod with the given filename. + int i = 0; + for (auto iter = begin(); iter != end(); ++iter, i++) + { + if (iter->GetFileName() == QFileInfo(filename)) + return i; + } + + // If nothing is found, return nullptr. + return -1; +} + +Mod* ModList::FindByID(const QString& modID, const QString& modVersion) +{ + // Search the list for a mod that matches + for (auto iter = begin(); iter != end(); ++iter) + { + QString ID = iter->GetModID(); + QString version = iter->GetModVersion(); + if ( ID == modID && version == modVersion) + return &(*iter); + } + + // If nothing is found, return nullptr. + return nullptr; +} + +void ModList::LoadFromFile(const QString& file) +{ + if (!wxFileExists(file)) + return; + + wxFFileInputStream inputStream(file); + wxArrayString modListFile = ReadAllLines(inputStream); + + for (wxArrayString::iterator iter = modListFile.begin(); iter != modListFile.end(); iter++) + { + // Normalize the path to the instMods dir. + wxFileName modFile(*iter); + modFile.Normalize(wxPATH_NORM_ALL, modsFolder); + modFile.MakeRelativeTo(); + // if the file is gone, do not load it + if(!modFile.Exists()) + { + continue; + } + + if (FindByFilename(modFile.GetFullPath()) == nullptr) + { + push_back(Mod(modFile)); + } + } +} + +void ModList::SaveToFile(const QString& file) +{ + QString text; + for (iterator iter = begin(); iter != end(); ++iter) + { + wxFileName modFile = iter->GetFileName(); + modFile.MakeRelativeTo(modsFolder); + text.append(modFile.GetFullPath()); + text.append("\n"); + } + + wxTempFileOutputStream out(file); + WriteAllText(out, text); + out.Commit(); +} + +bool ModList::InsertMod(size_t index, const QString &filename, const QString& saveToFile) +{ + QFileInfo source(filename); + QFileInfo dest(PathCombine(modsFolder, source.fileName())); + + if (source != dest) + { + QFile::copy(source.absoluteFilePath(), dest.absoluteFilePath()); + } + + int oldIndex = FindIndexByFilename(dest.absoluteFilePath()); + + if (oldIndex != -1) + { + erase(begin() + oldIndex); + } + + if (index >= size()) + push_back(Mod(dest)); + else + insert(begin() + index, Mod(dest)); + + if (!saveToFile.isEmpty()) + SaveToFile(saveToFile); + + return true; +} + +bool ModList::DeleteMod(size_t index, const QString& saveToFile) +{ + Mod *mod = &at(index); + if(mod->GetModType() == Mod::MOD_FOLDER) + { + QDir dir(mod->GetFileName().absoluteFilePath()); + if(dir.removeRecursively()) + { + erase(begin() + index); + + if (!saveToFile.isEmpty()) + SaveToFile(saveToFile); + + return true; + } + else + { + // wxLogError(_("Failed to delete mod.")); + } + } + else if (QFile(mod->GetFileName().absoluteFilePath()).remove()) + { + erase(begin() + index); + + if (!saveToFile.isEmpty()) + SaveToFile(saveToFile); + + return true; + } + else + { + // wxLogError(_("Failed to delete mod.")); + } + return false; +} + +bool JarModList::InsertMod(size_t index, const QString &filename, const QString& saveToFile) +{ + QString saveFile = saveToFile; + if (saveToFile.isEmpty()) + saveFile = m_inst->GetModListFile().GetFullPath(); + + if (ModList::InsertMod(index, filename, saveFile)) + { + m_inst->setLWJGLVersion(true); + return true; + } + return false; +} + +bool JarModList::DeleteMod(size_t index, const QString& saveToFile) +{ + QString saveFile = saveToFile; + if (saveToFile.IsEmpty()) + saveFile = m_inst->GetModListFile().GetFullPath(); + + if (ModList::DeleteMod(index, saveFile)) + { + m_inst->SetNeedsRebuild(); + return true; + } + return false; +} + +bool JarModList::UpdateModList(bool quickLoad) +{ + if (ModList::UpdateModList(quickLoad)) + { + m_inst->SetNeedsRebuild(); + return true; + } + return false; +} + +bool FolderModList::LoadModListFromDir(const QString& loadFrom, bool quickLoad) +{ + QString dir(loadFrom.IsEmpty() ? modsFolder : loadFrom); + + if (!wxDirExists(dir)) + return false; + + bool listChanged = false; + wxDir modDir(dir); + + if (!modDir.IsOpened()) + { + wxLogError(_("Failed to open directory: ") + dir); + return false; + } + + QString currentFile; + if (modDir.GetFirst(¤tFile)) + { + do + { + wxFileName modFile(Path::Combine(dir, currentFile)); + + if (wxFileExists(modFile.GetFullPath()) || wxDirExists(modFile.GetFullPath())) + { + if (quickLoad || FindByFilename(modFile.GetFullPath()) == nullptr) + { + Mod mod(modFile.GetFullPath()); + push_back(mod); + listChanged = true; + } + } + } while (modDir.GetNext(¤tFile)); + } + + return listChanged; +} + +bool ModNameSort (const Mod & i,const Mod & j) +{ + if(i.GetModType() == j.GetModType()) + return (i.GetName().toLower() < j.GetName().toLower()); + return (i.GetModType() < j.GetModType()); +} + +bool FolderModList::UpdateModList ( bool quickLoad ) +{ + bool changed = ModList::UpdateModList(quickLoad); + std::sort(begin(),end(),ModNameSort); + return changed; +} +*/ diff --git a/logic/ModList.h b/logic/ModList.h new file mode 100644 index 00000000..41d26491 --- /dev/null +++ b/logic/ModList.h @@ -0,0 +1,67 @@ +// +// Copyright 2013 MultiMC Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +#pragma once + +class LegacyInstance; +class BaseInstance; +#include +#include +#include +#include + +#include "Mod.h" + +/** + * A legacy mod list. + * Backed by a folder. + */ +class ModList : public QAbstractListModel +{ + Q_OBJECT +public: + ModList(const QString& dir, const QString& list_file); + + virtual QVariant data ( const QModelIndex& index, int role = Qt::DisplayRole ) const; + virtual int rowCount ( const QModelIndex& parent = QModelIndex() ) const + { + return size(); + }; + virtual QVariant headerData ( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + virtual int columnCount ( const QModelIndex& parent ) const; + + size_t size() const { return mods.size(); }; + Mod& operator[](size_t index) { return mods[index]; }; + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + virtual bool installMod(const QFileInfo& filename, size_t index = 0); + + /// Deletes the mod at the given index. + virtual bool deleteMod(size_t index); + + /** + * move the mod at index to the position N + * 0 is the beginning of the list, length() is the end of the list. + */ + virtual bool moveMod(size_t from, size_t to); + + virtual bool isValid(); + +signals: + void changed(); +protected: + QDir m_dir; + QString m_list_file; + QList mods; +}; diff --git a/logic/NostalgiaInstance.cpp b/logic/NostalgiaInstance.cpp new file mode 100644 index 00000000..0a7f3c5a --- /dev/null +++ b/logic/NostalgiaInstance.cpp @@ -0,0 +1,12 @@ +#include "NostalgiaInstance.h" + +NostalgiaInstance::NostalgiaInstance ( const QString& rootDir, SettingsObject* settings, QObject* parent ) + : OneSixInstance ( rootDir, settings, parent ) +{ + +} + +/* +ADD MORE + IF REQUIRED +*/ diff --git a/logic/NostalgiaInstance.h b/logic/NostalgiaInstance.h new file mode 100644 index 00000000..b8858218 --- /dev/null +++ b/logic/NostalgiaInstance.h @@ -0,0 +1,10 @@ +#pragma once + +#include "OneSixInstance.h" + +class NostalgiaInstance : public OneSixInstance +{ + Q_OBJECT +public: + explicit NostalgiaInstance(const QString &rootDir, SettingsObject * settings, QObject *parent = 0); +}; diff --git a/logic/OneSixAssets.cpp b/logic/OneSixAssets.cpp new file mode 100644 index 00000000..db9e7421 --- /dev/null +++ b/logic/OneSixAssets.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include "OneSixAssets.h" +#include "net/DownloadJob.h" + +inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname) +{ + QDomNodeList elementList = parent.elementsByTagName(tagname); + if (elementList.count()) + return elementList.at(0).toElement(); + else + return QDomElement(); +} + +class ThreadedDeleter : public QThread +{ + Q_OBJECT +public: + void run() + { + QDirIterator iter(m_base, QDirIterator::Subdirectories); + QStringList nuke_list; + int base_length = m_base.length(); + while (iter.hasNext()) + { + QString filename = iter.next(); + QFileInfo current(filename); + // we keep the dirs... whatever + if(current.isDir()) + continue; + QString trimmedf = filename; + trimmedf.remove(0, base_length + 1); + if(m_whitelist.contains(trimmedf)) + { + // qDebug() << trimmedf << " gets to live"; + } + else + { + // DO NOT TOLERATE JUNK + // qDebug() << trimmedf << " dies"; + QFile f (filename); + f.remove(); + } + } + }; + QString m_base; + QStringList m_whitelist; +}; + +class NukeAndPaveJob: public Job +{ + Q_OBJECT +public: + + explicit NukeAndPaveJob(QString base, QStringList whitelist) + :Job() + { + QDir dir(base); + deleterThread.m_base = dir.absolutePath(); + deleterThread.m_whitelist = whitelist; + }; +public slots: + virtual void start() + { + connect(&deleterThread, SIGNAL(finished()), SLOT(threadFinished())); + deleterThread.start(); + }; + void threadFinished() + { + emit finish(); + } +private: + ThreadedDeleter deleterThread; +}; + + + +void OneSixAssets::fetchFinished() +{ + QString prefix ( "http://s3.amazonaws.com/Minecraft.Resources/" ); + QString fprefix ( "assets/" ); + QStringList nuke_whitelist; + + JobPtr firstJob = index_job->getFirstJob(); + auto DlJob = firstJob.dynamicCast(); + QByteArray ba = DlJob->m_data; + + QString xmlErrorMsg; + QDomDocument doc; + if ( !doc.setContent ( ba, false, &xmlErrorMsg ) ) + { + qDebug() << "Failed to process s3.amazonaws.com/Minecraft.Resources. XML error:" << + xmlErrorMsg << ba; + } + //QRegExp etag_match(".*([a-f0-9]{32}).*"); + QDomNodeList contents = doc.elementsByTagName ( "Contents" ); + + JobList *job = new JobList(); + connect ( job, SIGNAL ( finished() ), SIGNAL(finished()) ); + connect ( job, SIGNAL ( failed() ), SIGNAL(failed()) ); + + for ( int i = 0; i < contents.length(); i++ ) + { + QDomElement element = contents.at ( i ).toElement(); + + if ( element.isNull() ) + continue; + + QDomElement keyElement = getDomElementByTagName ( element, "Key" ); + QDomElement lastmodElement = getDomElementByTagName ( element, "LastModified" ); + QDomElement etagElement = getDomElementByTagName ( element, "ETag" ); + QDomElement sizeElement = getDomElementByTagName ( element, "Size" ); + + if ( keyElement.isNull() || lastmodElement.isNull() || etagElement.isNull() || sizeElement.isNull() ) + continue; + + QString keyStr = keyElement.text(); + QString lastModStr = lastmodElement.text(); + QString etagStr = etagElement.text(); + QString sizeStr = sizeElement.text(); + + //Filter folder keys + if ( sizeStr == "0" ) + continue; + + QString filename = fprefix + keyStr; + QFile check_file ( filename ); + QString client_etag = "nonsense"; + // if there already is a file and md5 checking is in effect and it can be opened + if ( check_file.exists() && check_file.open ( QIODevice::ReadOnly ) ) + { + // check the md5 against the expected one + client_etag = QCryptographicHash::hash ( check_file.readAll(), QCryptographicHash::Md5 ).toHex().constData(); + check_file.close(); + } + + QString trimmedEtag = etagStr.remove ( '"' ); + nuke_whitelist.append ( keyStr ); + if(trimmedEtag != client_etag) + job->add ( DownloadJob::create ( QUrl ( prefix + keyStr ), filename ) ); + + } + job->add ( JobPtr ( new NukeAndPaveJob ( fprefix, nuke_whitelist ) ) ); + files_job.reset ( job ); + dl.enqueue ( files_job ); +} +void OneSixAssets::fetchStarted() +{ + qDebug() << "Started downloading!"; +} +void OneSixAssets::start() +{ + JobList *job = new JobList(); + job->add ( DownloadJob::create ( QUrl ( "http://s3.amazonaws.com/Minecraft.Resources/" ) ) ); + connect ( job, SIGNAL ( finished() ), SLOT ( fetchFinished() ) ); + connect ( job, SIGNAL ( started() ), SLOT ( fetchStarted() ) ); + index_job.reset ( job ); + dl.enqueue ( index_job ); +} + +#include "OneSixAssets.moc" \ No newline at end of file diff --git a/logic/OneSixAssets.h b/logic/OneSixAssets.h new file mode 100644 index 00000000..8c345daa --- /dev/null +++ b/logic/OneSixAssets.h @@ -0,0 +1,22 @@ +#pragma once +#include "net/DownloadJob.h" + +class Private; + +class OneSixAssets : public QObject +{ + Q_OBJECT +signals: + void failed(); + void finished(); + +public slots: + void fetchFinished(); + void fetchStarted(); +public: + void start(); +private: + JobListQueue dl; + JobListPtr index_job; + JobListPtr files_job; +}; diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp new file mode 100644 index 00000000..b7f39e4a --- /dev/null +++ b/logic/OneSixInstance.cpp @@ -0,0 +1,218 @@ +#include "OneSixInstance.h" +#include "OneSixInstance_p.h" +#include "OneSixUpdate.h" +#include "MinecraftProcess.h" +#include "VersionFactory.h" + +#include +#include +#include +#include + +OneSixInstance::OneSixInstance ( const QString& rootDir, SettingsObject* setting_obj, QObject* parent ) +: BaseInstance ( new OneSixInstancePrivate(), rootDir, setting_obj, parent ) +{ + I_D(OneSixInstance); + d->m_settings->registerSetting(new Setting("IntendedVersion", "")); + d->m_settings->registerSetting(new Setting("ShouldUpdate", false)); + reloadFullVersion(); +} + +BaseUpdate* OneSixInstance::doUpdate() +{ + return 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( QString user, QString session ) +{ + I_D(OneSixInstance); + auto version = d->version; + QString args_pattern = version->minecraftArguments; + + QMap token_mapping; + token_mapping["auth_username"] = user; + token_mapping["auth_session"] = session; + //FIXME: user and player name are DIFFERENT! + token_mapping["auth_player_name"] = user; + //FIXME: WTF is this. I just plugged in a random UUID here. + token_mapping["auth_uuid"] = "7d4bacf0-fd62-11e2-b778-0800200c9a66"; // obviously fake. + + // this is for offline: + /* + map["auth_player_name"] = "Player"; + map["auth_player_name"] = "00000000-0000-0000-0000-000000000000"; + */ + + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = version->id; + + QString absRootDir = QDir(minecraftRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + token_mapping["game_assets"] = absAssetsDir; + + QStringList parts = args_pattern.split(' ',QString::SkipEmptyParts); + for (int i = 0; i < parts.length(); i++) + { + parts[i] = replaceTokensIn(parts[i], token_mapping); + } + return parts; +} + +MinecraftProcess* OneSixInstance::prepareForLaunch ( QString user, QString session ) +{ + I_D(OneSixInstance); + cleanupAfterRun(); + auto version = d->version; + if(!version) + return nullptr; + auto libs_to_extract = version->getActiveNativeLibs(); + QString natives_dir_raw = PathCombine(instanceRoot(), "natives/"); + bool success = ensurePathExists(natives_dir_raw); + if(!success) + { + // FIXME: handle errors + return nullptr; + } + + for(auto lib: libs_to_extract) + { + QString path = "libraries/" + lib->storagePath(); + qDebug() << "Will extract " << path.toLocal8Bit(); + if(JlCompress::extractWithExceptions(path, natives_dir_raw, lib->extract_excludes).isEmpty()) + { + return nullptr; + } + } + + QStringList args; + args.append(Util::Commandline::splitArgs(settings().get("JvmArgs").toString())); + args << QString("-Xms%1m").arg(settings().get("MinMemAlloc").toInt()); + args << QString("-Xmx%1m").arg(settings().get("MaxMemAlloc").toInt()); + QDir natives_dir(natives_dir_raw); + args << QString("-Djava.library.path=%1").arg( natives_dir.absolutePath() ); + QString classPath; + { + auto libs = version->getActiveNormalLibs(); + for (auto lib: libs) + { + QFileInfo fi(QString("libraries/") + lib->storagePath()); + classPath.append(fi.absoluteFilePath()); + //FIXME: make separator tweakable + classPath.append(':'); + } + QString targetstr = "versions/" + version->id + "/" + version->id + ".jar"; + QFileInfo fi(targetstr); + classPath.append(fi.absoluteFilePath()); + } + if(classPath.size()) + { + args << "-cp"; + args << classPath; + } + args << version->mainClass; + args.append(processMinecraftArgs(user, session)); + + // create the process and set its parameters + MinecraftProcess * proc = new MinecraftProcess(this); + proc->setMinecraftArguments(args); + proc->setMinecraftWorkdir(minecraftRoot()); + return proc; +} + +void OneSixInstance::cleanupAfterRun() +{ + QString target_dir = PathCombine(instanceRoot(), "natives/"); + QDir dir(target_dir); + dir.removeRecursively(); +} + +QSharedPointer< QDialog > OneSixInstance::createModEditDialog ( QWidget* parent ) +{ + return QSharedPointer< QDialog >(); +} + + + +bool OneSixInstance::setIntendedVersionId ( QString version ) +{ + settings().set("IntendedVersion", version); + setShouldUpdate(true); + return true; +} + +QString OneSixInstance::intendedVersionId() const +{ + return settings().get("IntendedVersion").toString(); +} + +void OneSixInstance::setShouldUpdate ( bool val ) +{ + settings().set ( "ShouldUpdate", val ); +} + +bool OneSixInstance::shouldUpdate() const +{ + I_D(OneSixInstance); + QVariant var = settings().get ( "ShouldUpdate" ); + if ( !var.isValid() || var.toBool() == false ) + { + return intendedVersionId() != currentVersionId(); + } + return true; +} + +QString OneSixInstance::currentVersionId() const +{ + return intendedVersionId(); +} + +bool OneSixInstance::reloadFullVersion() +{ + I_D(OneSixInstance); + + QString verpath = PathCombine(instanceRoot(), "version.json"); + QFile versionfile(verpath); + if(versionfile.exists() && versionfile.open(QIODevice::ReadOnly)) + { + FullVersionFactory fvf; + auto version = fvf.parse(versionfile.readAll()); + versionfile.close(); + if(version) + { + d->version = version; + return true; + } + }; + return false; +} + +QSharedPointer< FullVersion > OneSixInstance::getFullVersion() +{ + I_D(OneSixInstance); + return d->version; +} diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h new file mode 100644 index 00000000..edd96eaa --- /dev/null +++ b/logic/OneSixInstance.h @@ -0,0 +1,34 @@ +#pragma once + +#include "BaseInstance.h" +#include +class FullVersion; +class BaseUpdate; + +class OneSixInstance : public BaseInstance +{ + Q_OBJECT +public: + explicit OneSixInstance(const QString &rootDir, SettingsObject * settings, QObject *parent = 0); + virtual BaseUpdate* doUpdate(); + virtual MinecraftProcess* prepareForLaunch ( QString user, QString session ); + virtual void cleanupAfterRun(); + + virtual QString intendedVersionId() const; + virtual bool setIntendedVersionId ( QString version ); + + virtual QString currentVersionId() const; + // virtual void setCurrentVersionId ( QString val ) {}; + + virtual bool shouldUpdate() const; + virtual void setShouldUpdate(bool val); + + virtual QSharedPointer< QDialog > createModEditDialog ( QWidget* parent ); + + /// reload the full version json file. return true on success! + bool reloadFullVersion(); + /// get the current full version info + QSharedPointer getFullVersion(); +private: + QStringList processMinecraftArgs( QString user, QString session ); +}; \ No newline at end of file diff --git a/logic/OneSixInstance_p.h b/logic/OneSixInstance_p.h new file mode 100644 index 00000000..1037e03c --- /dev/null +++ b/logic/OneSixInstance_p.h @@ -0,0 +1,9 @@ +#pragma once + +#include "BaseInstance_p.h" +#include "OneSixVersion.h" + +struct OneSixInstancePrivate: public BaseInstancePrivate +{ + QSharedPointer version; +}; \ No newline at end of file diff --git a/logic/OneSixUpdate.cpp b/logic/OneSixUpdate.cpp new file mode 100644 index 00000000..2bb2f496 --- /dev/null +++ b/logic/OneSixUpdate.cpp @@ -0,0 +1,169 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OneSixUpdate.h" + +#include + +#include +#include +#include +#include + +#include + +#include "BaseInstance.h" +#include "lists/MinecraftVersionList.h" +#include "VersionFactory.h" +#include "OneSixVersion.h" +#include "OneSixInstance.h" + +#include "pathutils.h" + + +OneSixUpdate::OneSixUpdate(BaseInstance *inst, QObject *parent):BaseUpdate(inst, parent){} + +void OneSixUpdate::executeTask() +{ + QString intendedVersion = m_inst->intendedVersionId(); + + // Make directories + QDir mcDir(m_inst->minecraftRoot()); + if (!mcDir.exists() && !mcDir.mkpath(".")) + { + emitFailed("Failed to create bin folder."); + return; + } + + // Get a pointer to the version object that corresponds to the instance's version. + targetVersion = MinecraftVersionList::getMainList().findVersion(intendedVersion).dynamicCast(); + if(targetVersion == nullptr) + { + // don't do anything if it was invalid + emitSucceeded(); + return; + } + + if(m_inst->shouldUpdate()) + { + versionFileStart(); + } + else + { + jarlibStart(); + } +} + +void OneSixUpdate::versionFileStart() +{ + setStatus("Getting the version files from Mojang."); + + QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/"); + urlstr += targetVersion->descriptor + "/" + targetVersion->descriptor + ".json"; + auto dljob = DownloadJob::create(QUrl(urlstr)); + specificVersionDownloadJob.reset(new JobList()); + specificVersionDownloadJob->add(dljob); + connect(specificVersionDownloadJob.data(), SIGNAL(finished()), SLOT(versionFileFinished())); + connect(specificVersionDownloadJob.data(), SIGNAL(failed()), SLOT(versionFileFailed())); + connect(specificVersionDownloadJob.data(), SIGNAL(progress(qint64,qint64)), SLOT(updateDownloadProgress(qint64,qint64))); + download_queue.enqueue(specificVersionDownloadJob); +} + +void OneSixUpdate::versionFileFinished() +{ + JobPtr firstJob = specificVersionDownloadJob->getFirstJob(); + auto DlJob = firstJob.dynamicCast(); + + QString version_id = targetVersion->descriptor; + QString inst_dir = m_inst->instanceRoot(); + // save the version file in $instanceId/version.json + { + QString version1 = PathCombine(inst_dir, "/version.json"); + ensurePathExists(version1); + // FIXME: detect errors here, download to a temp file, swap + QFile vfile1 (version1); + vfile1.open(QIODevice::Truncate | QIODevice::WriteOnly ); + vfile1.write(DlJob->m_data); + vfile1.close(); + } + + // the version is downloaded safely. update is 'done' at this point + m_inst->setShouldUpdate(false); + // save the version file in versions/$version/$version.json + /* + //QString version2 = QString("versions/") + version_id + "/" + version_id + ".json"; + //ensurePathExists(version2); + //QFile vfile2 (version2); + //vfile2.open(QIODevice::Truncate | QIODevice::WriteOnly ); + //vfile2.write(DlJob->m_data); + //vfile2.close(); + */ + + jarlibStart(); +} + +void OneSixUpdate::versionFileFailed() +{ + emitFailed("Failed to download the version description. Try again."); +} + +void OneSixUpdate::jarlibStart() +{ + OneSixInstance * inst = (OneSixInstance *) m_inst; + bool successful = inst->reloadFullVersion(); + if(!successful) + { + emitFailed("Failed to load the version description file (version.json). It might be corrupted, missing or simply too new."); + return; + } + + QSharedPointer version = inst->getFullVersion(); + + // download the right jar, save it in versions/$version/$version.jar + QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/"); + urlstr += version->id + "/" + version->id + ".jar"; + QString targetstr ("versions/"); + targetstr += version->id + "/" + version->id + ".jar"; + + auto dljob = DownloadJob::create(QUrl(urlstr), targetstr); + jarlibDownloadJob.reset(new JobList()); + jarlibDownloadJob->add(dljob); + + auto libs = version->getActiveNativeLibs(); + libs.append(version->getActiveNormalLibs()); + + for(auto lib: libs) + { + QString download_path = lib->downloadPath(); + QString storage_path = "libraries/" + lib->storagePath(); + jarlibDownloadJob->add(DownloadJob::create(download_path, storage_path)); + } + connect(jarlibDownloadJob.data(), SIGNAL(finished()), SLOT(jarlibFinished())); + connect(jarlibDownloadJob.data(), SIGNAL(failed()), SLOT(jarlibFailed())); + connect(jarlibDownloadJob.data(), SIGNAL(progress(qint64,qint64)), SLOT(updateDownloadProgress(qint64,qint64))); + + download_queue.enqueue(jarlibDownloadJob); +} + +void OneSixUpdate::jarlibFinished() +{ + emitSucceeded(); +} + +void OneSixUpdate::jarlibFailed() +{ + emitFailed("Failed to download the binary garbage. Try again. Maybe. IF YOU DARE"); +} + diff --git a/logic/OneSixUpdate.h b/logic/OneSixUpdate.h new file mode 100644 index 00000000..7a0cac52 --- /dev/null +++ b/logic/OneSixUpdate.h @@ -0,0 +1,54 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include "net/DownloadJob.h" + +#include "tasks/Task.h" +#include "BaseUpdate.h" + +class MinecraftVersion; +class BaseInstance; + +class OneSixUpdate : public BaseUpdate +{ + Q_OBJECT +public: + explicit OneSixUpdate(BaseInstance *inst, QObject *parent = 0); + virtual void executeTask(); + +private slots: + void versionFileStart(); + void versionFileFinished(); + void versionFileFailed(); + + void jarlibStart(); + void jarlibFinished(); + void jarlibFailed(); + +private: + JobListPtr specificVersionDownloadJob; + JobListPtr jarlibDownloadJob; + JobListQueue download_queue; + + // target version, determined during this task + QSharedPointer targetVersion; +}; + + diff --git a/logic/OneSixVersion.cpp b/logic/OneSixVersion.cpp new file mode 100644 index 00000000..2b2f79f5 --- /dev/null +++ b/logic/OneSixVersion.cpp @@ -0,0 +1,132 @@ +#include "OneSixVersion.h" + +RuleAction RuleAction_fromString(QString name) +{ + if(name == "allow") + return Allow; + if(name == "disallow") + return Disallow; + return Defer; +} + +OpSys OpSys_fromString(QString name) +{ + if(name == "linux") + return Os_Linux; + if(name == "windows") + return Os_Windows; + if(name == "osx") + return Os_OSX; + return Os_Other; +} + +void Library::finalize() +{ + QStringList parts = m_name.split ( ':' ); + QString relative = parts[0]; + relative.replace ( '.','/' ); + relative += '/' + parts[1] + '/' + parts[2] + '/' + parts[1] + '-' + parts[2]; + if ( !m_is_native ) + relative += ".jar"; + else + { + if ( m_native_suffixes.contains ( currentSystem ) ) + { + relative += "-" + m_native_suffixes[currentSystem] + ".jar"; + } + else + { + // really, bad. + relative += ".jar"; + } + } + m_storage_path = relative; + m_download_path = m_base_url + relative; + + if ( m_rules.empty() ) + { + m_is_active = true; + } + else + { + RuleAction result = Disallow; + for ( auto rule: m_rules ) + { + RuleAction temp = rule->apply ( this ); + if ( temp != Defer ) + result = temp; + } + m_is_active = ( result == Allow ); + } + if ( m_is_native ) + { + m_is_active = m_is_active && m_native_suffixes.contains ( currentSystem ); + } +} + +void Library::setName ( QString name ) +{ + m_name = name; +} +void Library::setBaseUrl ( QString base_url ) +{ + m_base_url = base_url; +} +void Library::setIsNative() +{ + m_is_native = true; +} +void Library::addNative ( OpSys os, QString suffix ) +{ + m_is_native = true; + m_native_suffixes[os] = suffix; +} +void Library::setRules ( QList< QSharedPointer< Rule > > rules ) +{ + m_rules = rules; +} +bool Library::isActive() +{ + return m_is_active; +} +bool Library::isNative() +{ + return m_is_native; +} +QString Library::downloadPath() +{ + return m_download_path; +} +QString Library::storagePath() +{ + return m_storage_path; +} + + +QList > FullVersion::getActiveNormalLibs() +{ + QList > output; + for ( auto lib: libraries ) + { + if (lib->isActive() && !lib->isNative()) + { + output.append(lib); + } + } + return output; +} + +QList > FullVersion::getActiveNativeLibs() +{ + QList > output; + for ( auto lib: libraries ) + { + if (lib->isActive() && lib->isNative()) + { + output.append(lib); + } + } + return output; +} + + diff --git a/logic/OneSixVersion.h b/logic/OneSixVersion.h new file mode 100644 index 00000000..e4f75542 --- /dev/null +++ b/logic/OneSixVersion.h @@ -0,0 +1,212 @@ +#pragma once +#include + +class Library; + +enum OpSys +{ + Os_Windows, + Os_Linux, + Os_OSX, + Os_Other +}; + +OpSys OpSys_fromString(QString); + +#ifdef Q_OS_WIN32 + #define currentSystem Os_Windows +#elif Q_OS_MAC + #define currentSystem Os_OSX +#else + #define currentSystem Os_Linux +#endif + +enum RuleAction +{ + Allow, + Disallow, + Defer +}; + +RuleAction RuleAction_fromString(QString); + +class Rule +{ +protected: + RuleAction m_result; + virtual bool applies(Library * parent) = 0; +public: + Rule(RuleAction result) + :m_result(result) {} + virtual ~Rule(){}; + RuleAction apply(Library * parent) + { + if(applies(parent)) + return m_result; + else + return Defer; + }; +}; + +class OsRule : public Rule +{ +private: + // the OS + OpSys m_system; + // the OS version regexp + QString m_version_regexp; +protected: + virtual bool applies ( Library* ) + { + return (m_system == currentSystem); + } + OsRule(RuleAction result, OpSys system, QString version_regexp) + : Rule(result), m_system(system), m_version_regexp(version_regexp) {} +public: + static QSharedPointer create(RuleAction result, OpSys system, QString version_regexp) + { + return QSharedPointer (new OsRule(result, system, version_regexp)); + } +}; + +class ImplicitRule : public Rule +{ +protected: + virtual bool applies ( Library* ) + { + return true; + } + ImplicitRule(RuleAction result) + : Rule(result) {} +public: + static QSharedPointer create(RuleAction result) + { + return QSharedPointer (new ImplicitRule(result)); + } +}; + +class Library +{ +private: + // basic values used internally (so far) + QString m_name; + QString m_base_url; + QList > m_rules; + + // derived values used for real things + /// where to store the lib locally + QString m_storage_path; + /// where to download the lib from + QString m_download_path; + /// is this lib actually active on the current OS? + bool m_is_active; + /// is the library a native? + bool m_is_native; + /// native suffixes per OS + QMap m_native_suffixes; +public: + QStringList extract_excludes; + +public: + /// Constructor + Library(QString name) + { + m_is_native = false; + m_is_native = false; + m_name = name; + m_base_url = "https://s3.amazonaws.com/Minecraft.Download/libraries/"; + } + + /** + * finalize the library, processing the input values into derived values and state + * + * This SHALL be called after all the values are parsed or after any further change. + */ + void finalize(); + + /// Set the library composite name + void setName(QString name); + /// Set the url base for downloads + void setBaseUrl(QString base_url); + /// Call this to mark the library as 'native' (it's a zip archive with DLLs) + void setIsNative(); + /// Attach a name suffix to the specified OS native + void addNative(OpSys os, QString suffix); + /// Set the load rules + void setRules(QList > rules); + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive(); + /// Returns true if the library is native + bool isNative(); + /// Get the URL to download the library from + QString downloadPath(); + /// Get the relative path where the library should be saved + QString storagePath(); +}; + + +class FullVersion +{ +public: + /// the ID - determines which jar to use! ACTUALLY IMPORTANT! + QString id; + /// Last updated time - as a string + QString time; + /// Release time - as a string + QString releaseTime; + /// Release type - "release" or "snapshot" + QString type; + /** + * DEPRECATED: Old versions of the new vanilla launcher used this + * ex: "username_session_version" + */ + QString processArguments; + /** + * arguments that should be used for launching minecraft + * + * ex: "--username ${auth_player_name} --session ${auth_session} + * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" + */ + QString minecraftArguments; + /** + * the minimum launcher version required by this version ... current is 4 (at point of writing) + */ + int minimumLauncherVersion; + /** + * The main class to load first + */ + QString mainClass; + + /// the list of libs - both active and inactive, native and java + QList > libraries; + + /* + FIXME: add support for those rules here? Looks like a pile of quick hacks to me though. + + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx", + "version": "^10\\.5\\.\\d$" + } + } + ], + "incompatibilityReason": "There is a bug in LWJGL which makes it incompatible with OSX 10.5.8. Please go to New Profile and use 1.5.2 for now. Sorry!" + } + */ + // QList rules; + +public: + FullVersion() + { + minimumLauncherVersion = 0xDEADBEEF; + } + + QList > getActiveNormalLibs(); + QList > getActiveNativeLibs(); +}; \ No newline at end of file diff --git a/logic/VersionFactory.cpp b/logic/VersionFactory.cpp new file mode 100644 index 00000000..9eccce26 --- /dev/null +++ b/logic/VersionFactory.cpp @@ -0,0 +1,195 @@ +#include "VersionFactory.h" +#include "OneSixVersion.h" + +// Library rules (if any) +QList > FullVersionFactory::parse4rules(QJsonObject & baseObj) +{ + QList > rules; + auto rulesVal = baseObj.value("rules"); + if(rulesVal.isArray()) + { + QJsonArray ruleList = rulesVal.toArray(); + for(auto ruleVal : ruleList) + { + QSharedPointer rule; + if(!ruleVal.isObject()) + continue; + auto ruleObj = ruleVal.toObject(); + auto actionVal = ruleObj.value("action"); + if(!actionVal.isString()) + continue; + auto action = RuleAction_fromString(actionVal.toString()); + if(action == Defer) + continue; + + auto osVal = ruleObj.value("os"); + if(!osVal.isObject()) + { + // add a new implicit action rule + rules.append(ImplicitRule::create(action)); + } + else + { + auto osObj = osVal.toObject(); + auto osNameVal = osObj.value("name"); + if(!osNameVal.isString()) + continue; + OpSys requiredOs = OpSys_fromString(osNameVal.toString()); + QString versionRegex = osObj.value("version").toString(); + // add a new OS rule + rules.append(OsRule::create(action, requiredOs, versionRegex)); + } + } + } + return rules; +} + + +QSharedPointer FullVersionFactory::parse4(QJsonObject root, QSharedPointer fullVersion) +{ + fullVersion->id = root.value("id").toString(); + + fullVersion->mainClass = root.value("mainClass").toString(); + auto procArgsValue = root.value("processArguments"); + if(procArgsValue.isString()) + { + fullVersion->processArguments = procArgsValue.toString(); + QString toCompare = fullVersion->processArguments.toLower(); + if(toCompare == "legacy") + { + fullVersion->minecraftArguments = " ${auth_player_name} ${auth_session}"; + } + else if(toCompare == "username_session") + { + fullVersion->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}"; + } + else if(toCompare == "username_session_version") + { + fullVersion->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}"; + } + } + + auto minecraftArgsValue = root.value("minecraftArguments"); + if(minecraftArgsValue.isString()) + { + fullVersion->minecraftArguments = minecraftArgsValue.toString(); + } + + auto minecraftTypeValue = root.value("type"); + if(minecraftTypeValue.isString()) + { + fullVersion->type = minecraftTypeValue.toString(); + } + + fullVersion->releaseTime = root.value("releaseTime").toString(); + fullVersion->time = root.value("time").toString(); + + // Iterate through the list, if it's a list. + auto librariesValue = root.value("libraries"); + if(!librariesValue.isArray()) + return fullVersion; + + QJsonArray libList = root.value("libraries").toArray(); + for (auto libVal : libList) + { + if (!libVal.isObject()) + { + continue; + } + + QJsonObject libObj = libVal.toObject(); + + // Library name + auto nameVal = libObj.value("name"); + if(!nameVal.isString()) + continue; + QSharedPointer library(new Library(nameVal.toString())); + + auto urlVal = libObj.value("url"); + if(urlVal.isString()) + { + library->setBaseUrl(urlVal.toString()); + } + + // Extract excludes (if any) + auto extractVal = libObj.value("extract"); + if(extractVal.isObject()) + { + QStringList excludes; + auto extractObj = extractVal.toObject(); + auto excludesVal = extractObj.value("exclude"); + if(!excludesVal.isArray()) + goto SKIP_EXTRACTS; + auto excludesList = excludesVal.toArray(); + for(auto excludeVal : excludesList) + { + if(excludeVal.isString()) + excludes.append(excludeVal.toString()); + } + library->extract_excludes = excludes; + } + SKIP_EXTRACTS: + + auto nativesVal = libObj.value("natives"); + if(nativesVal.isObject()) + { + library->setIsNative(); + auto nativesObj = nativesVal.toObject(); + auto iter = nativesObj.begin(); + while(iter != nativesObj.end()) + { + auto osType = OpSys_fromString(iter.key()); + if(osType == Os_Other) + continue; + if(!iter.value().isString()) + continue; + library->addNative(osType, iter.value().toString()); + iter++; + } + } + library->setRules(parse4rules(libObj)); + library->finalize(); + fullVersion->libraries.append(library); + } + return fullVersion; +} + +QSharedPointer FullVersionFactory::parse(QByteArray data) +{ + QSharedPointer readVersion(new FullVersion()); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + error_string = QString( "Error reading version file :") + " " + jsonError.errorString(); + m_error = FullVersionFactory::ParseError; + return QSharedPointer(); + } + + if(!jsonDoc.isObject()) + { + error_string = "Error reading version file."; + m_error = FullVersionFactory::ParseError; + return QSharedPointer(); + } + QJsonObject root = jsonDoc.object(); + + int launcher_ver = readVersion->minimumLauncherVersion = root.value("minimumLauncherVersion").toDouble(); + // ADD MORE HERE :D + if(launcher_ver > 0 && launcher_ver <= 7) + return parse4(root, readVersion); + else + { + error_string = "Version file was for an unrecognized launcher version. RIP"; + m_error = FullVersionFactory::UnsupportedVersion; + return QSharedPointer(); + } +} + + +FullVersionFactory::FullVersionFactory() +{ + m_error = FullVersionFactory::AllOK; +} diff --git a/logic/VersionFactory.h b/logic/VersionFactory.h new file mode 100644 index 00000000..82c5278a --- /dev/null +++ b/logic/VersionFactory.h @@ -0,0 +1,24 @@ +#pragma once +#include + +struct FullVersion; +class Rule; + +class FullVersionFactory +{ +public: + enum Error + { + AllOK, // all parsed OK + ParseError, // the file was corrupted somehow + UnsupportedVersion // the file was meant for a launcher version we don't support (yet) + } m_error; + QString error_string; + +public: + FullVersionFactory(); + QSharedPointer parse(QByteArray data); +private: + QSharedPointer parse4(QJsonObject root, QSharedPointer product); + QList > parse4rules(QJsonObject & baseObj); +}; \ No newline at end of file diff --git a/logic/lists/InstVersionList.cpp b/logic/lists/InstVersionList.cpp new file mode 100644 index 00000000..7dc67155 --- /dev/null +++ b/logic/lists/InstVersionList.cpp @@ -0,0 +1,129 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "logic/lists/InstVersionList.h" +#include "logic/InstanceVersion.h" + +InstVersionList::InstVersionList(QObject *parent) : + QAbstractListModel(parent) +{ +} + +InstVersionPtr InstVersionList::findVersion( const QString& descriptor ) +{ + for (int i = 0; i < count(); i++) + { + if (at(i)->descriptor == descriptor) + return at(i); + } + return InstVersionPtr(); +} + +InstVersionPtr InstVersionList::getLatestStable() const +{ + if (count() <= 0) + return InstVersionPtr(); + else + return at(0); +} + +QVariant InstVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + + InstVersionPtr version = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return version->name; + + case TypeColumn: + return version->typeString(); + + case TimeColumn: + return version->timestamp; + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return version->descriptor; + + case VersionPointerRole: + return qVariantFromValue(version); + + default: + return QVariant(); + } +} + +QVariant InstVersionList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: + return "Name"; + + case TypeColumn: + return "Type"; + + case TimeColumn: + return "Time"; + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return "The name of the version."; + + case TypeColumn: + return "The version's type."; + + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +int InstVersionList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int InstVersionList::columnCount(const QModelIndex &parent) const +{ + return 2; +} diff --git a/logic/lists/InstVersionList.h b/logic/lists/InstVersionList.h new file mode 100644 index 00000000..bc6aa5d4 --- /dev/null +++ b/logic/lists/InstVersionList.h @@ -0,0 +1,121 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "logic/InstanceVersion.h" + +class Task; + +/*! + * \brief Class that each instance type's version list derives from. + * Version lists are the lists that keep track of the available game versions + * for that instance. This list will not be loaded on startup. It will be loaded + * when the list's load function is called. Before using the version list, you + * should check to see if it has been loaded yet and if not, load the list. + * + * Note that this class also inherits from QAbstractListModel. Methods from that + * class determine how this version list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by plugins to + * change the behavior of the list. + */ +class InstVersionList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + VersionPointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // First column - Name + NameColumn = 0, + + // Second column - Type + TypeColumn, + + // Third column - Timestamp + TimeColumn + }; + + explicit InstVersionList(QObject *parent = 0); + + /*! + * \brief Gets a task that will reload the version list. + * Simply execute the task to load the list. + * The task returned by this function should reset the model when it's done. + * \return A pointer to a task that reloads the version list. + */ + virtual Task *getLoadTask() = 0; + + //! Checks whether or not the list is loaded. If this returns false, the list should be loaded. + virtual bool isLoaded() = 0; + + //! Gets the version at the given index. + virtual const InstVersionPtr at(int i) const = 0; + + //! Returns the number of versions in the list. + virtual int count() const = 0; + + + //////// 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; + + + /*! + * \brief Finds a version by its descriptor. + * \param The descriptor of the version to find. + * \return A const pointer to the version with the given descriptor. NULL if + * one doesn't exist. + */ + virtual InstVersionPtr findVersion(const QString &descriptor); + + /*! + * \brief Gets the latest stable version of this instance type. + * This is the version that will be selected by default. + * By default, this is simply the first version in the list. + */ + virtual InstVersionPtr getLatestStable() const; + + /*! + * Sorts the version list. + */ + virtual void sort() = 0; + +protected slots: + /*! + * Updates this list with the given list of versions. + * This is done by copying each version in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the versions are set to this + * version list. This can't be done in the load task, because the versions 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 versions and sets their parents correctly. + * \param versions List of versions whose parents should be set. + */ + virtual void updateListData(QList versions) = 0; +}; diff --git a/logic/lists/InstanceList.cpp b/logic/lists/InstanceList.cpp new file mode 100644 index 00000000..39f55f7b --- /dev/null +++ b/logic/lists/InstanceList.cpp @@ -0,0 +1,232 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logic/lists/InstanceList.h" +#include "logic/BaseInstance.h" +#include "logic/InstanceFactory.h" + +#include "pathutils.h" + +const static int GROUP_FILE_FORMAT_VERSION = 1; + +InstanceList::InstanceList(const QString &instDir, QObject *parent) : + QObject(parent), m_instDir("instances") +{ + +} + +void InstanceList::loadGroupList(QMap & groupMap) +{ + QString groupFileName = m_instDir + "/instgroups.json"; + + // if there's no group file, fail + if(!QFileInfo(groupFileName).exists()) + return; + + QFile groupFile(groupFileName); + + // if you can't open the file, fail + if (!groupFile.open(QIODevice::ReadOnly)) + { + // An error occurred. Ignore it. + qDebug("Failed to read instance group file."); + return; + } + + QTextStream in(&groupFile); + QString jsonStr = in.readAll(); + groupFile.close(); + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonStr.toUtf8(), &error); + + // if the json was bad, fail + if (error.error != QJsonParseError::NoError) + { + qWarning(QString("Failed to parse instance group file: %1 at offset %2"). + arg(error.errorString(), QString::number(error.offset)).toUtf8()); + return; + } + + // if the root of the json wasn't an object, fail + if (!jsonDoc.isObject()) + { + qWarning("Invalid group file. Root entry should be an object."); + return; + } + + QJsonObject rootObj = jsonDoc.object(); + + // Make sure the format version matches, otherwise fail. + if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION) + return; + + // Get the groups. if it's not an object, fail + if (!rootObj.value("groups").isObject()) + { + qWarning("Invalid group list JSON: 'groups' should be an object."); + return; + } + + // Iterate through all the groups. + QJsonObject groupMapping = rootObj.value("groups").toObject(); + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) + { + QString groupName = iter.key(); + + // If not an object, complain and skip to the next one. + if (!iter.value().isObject()) + { + qWarning(QString("Group '%1' in the group list should " + "be an object.").arg(groupName).toUtf8()); + continue; + } + + QJsonObject groupObj = iter.value().toObject(); + if (!groupObj.value("instances").isArray()) + { + qWarning(QString("Group '%1' in the group list is invalid. " + "It should contain an array " + "called 'instances'.").arg(groupName).toUtf8()); + continue; + } + + // Iterate through the list of instances in the group. + QJsonArray instancesArray = groupObj.value("instances").toArray(); + + for (QJsonArray::iterator iter2 = instancesArray.begin(); + iter2 != instancesArray.end(); iter2++) + { + groupMap[(*iter2).toString()] = groupName; + } + } +} + +InstanceList::InstListError InstanceList::loadList() +{ + // load the instance groups + QMap groupMap; + loadGroupList(groupMap); + + m_instances.clear(); + QDir dir(m_instDir); + QDirIterator iter(dir); + while (iter.hasNext()) + { + QString subDir = iter.next(); + if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists()) + continue; + + BaseInstance *instPtr = NULL; + auto &loader = InstanceFactory::get(); + auto error = loader.loadInstance(instPtr, subDir); + + switch(error) + { + case InstanceFactory::NoLoadError: + break; + case InstanceFactory::NotAnInstance: + break; + } + + if (error != InstanceFactory::NoLoadError && + error != InstanceFactory::NotAnInstance) + { + QString errorMsg = QString("Failed to load instance %1: "). + arg(QFileInfo(subDir).baseName()).toUtf8(); + + switch (error) + { + default: + errorMsg += QString("Unknown instance loader error %1"). + arg(error); + break; + } + qDebug(errorMsg.toUtf8()); + } + else if (!instPtr) + { + qDebug(QString("Error loading instance %1. Instance loader returned null."). + arg(QFileInfo(subDir).baseName()).toUtf8()); + } + else + { + QSharedPointer inst(instPtr); + auto iter = groupMap.find(inst->id()); + if(iter != groupMap.end()) + { + inst->setGroup((*iter)); + } + qDebug(QString("Loaded instance %1").arg(inst->name()).toUtf8()); + inst->setParent(this); + m_instances.append(inst); + connect(instPtr, SIGNAL(propertiesChanged(BaseInstance*)),this, SLOT(propertiesChanged(BaseInstance*))); + } + } + emit invalidated(); + return NoError; +} + +/// Clear all instances. Triggers notifications. +void InstanceList::clear() +{ + m_instances.clear(); + emit invalidated(); +}; + +/// Add an instance. Triggers notifications, returns the new index +int InstanceList::add(InstancePtr t) +{ + m_instances.append(t); + emit instanceAdded(count() - 1); + return count() - 1; +} + +InstancePtr InstanceList::getInstanceById(QString instId) +{ + QListIterator iter(m_instances); + InstancePtr inst; + while(iter.hasNext()) + { + inst = iter.next(); + if (inst->id() == instId) + break; + } + if (inst->id() != instId) + return InstancePtr(); + else + return iter.peekPrevious(); +} + +void InstanceList::propertiesChanged(BaseInstance * inst) +{ + for(int i = 0; i < m_instances.count(); i++) + { + if(inst == m_instances[i].data()) + { + emit instanceChanged(i); + break; + } + } +} diff --git a/logic/lists/InstanceList.h b/logic/lists/InstanceList.h new file mode 100644 index 00000000..82ef0ea4 --- /dev/null +++ b/logic/lists/InstanceList.h @@ -0,0 +1,91 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "logic/BaseInstance.h" + +class BaseInstance; + +class InstanceList : public QObject +{ + Q_OBJECT +private: + /*! + * \brief Get the instance groups + */ + void loadGroupList(QMap & groupList); + +public: + explicit InstanceList(const QString &instDir, QObject *parent = 0); + + /*! + * \brief Error codes returned by functions in the InstanceList class. + * NoError Indicates that no error occurred. + * UnknownError indicates that an unspecified error occurred. + */ + enum InstListError + { + NoError = 0, + UnknownError + }; + + QString instDir() const { return m_instDir; } + + /*! + * \brief Loads the instance list. Triggers notifications. + */ + InstListError loadList(); + + /*! + * \brief Get the instance at index + */ + InstancePtr at(int i) const + { + return m_instances.at(i); + }; + + /*! + * \brief Get the count of loaded instances + */ + int count() const + { + return m_instances.count(); + }; + + /// Clear all instances. Triggers notifications. + void clear(); + + /// Add an instance. Triggers notifications, returns the new index + int add(InstancePtr t); + + /// Get an instance by ID + InstancePtr getInstanceById (QString id); + +signals: + void instanceAdded(int index); + void instanceChanged(int index); + void invalidated(); + +private slots: + void propertiesChanged(BaseInstance * inst); + +protected: + QString m_instDir; + QList< InstancePtr > m_instances; +}; diff --git a/logic/lists/LwjglVersionList.cpp b/logic/lists/LwjglVersionList.cpp new file mode 100644 index 00000000..0e7b5a34 --- /dev/null +++ b/logic/lists/LwjglVersionList.cpp @@ -0,0 +1,206 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LwjglVersionList.h" +#include "logic/net/NetWorker.h" + +#include + +#include + +#include + +#define RSS_URL "http://sourceforge.net/api/file/index/project-id/58488/mtime/desc/rss" + +LWJGLVersionList mainVersionList; + +LWJGLVersionList &LWJGLVersionList::get() +{ + return mainVersionList; +} + + +LWJGLVersionList::LWJGLVersionList(QObject *parent) : + QAbstractListModel(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 = 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 "Version"; + + case Qt::ToolTipRole: + return "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 = NetWorker::spawn(); + reply = worker.get(QNetworkRequest(QUrl(RSS_URL))); + 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; + if (!doc.setContent(reply->readAll(), 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()) + { + qWarning() << "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; + } + + tempList.append(LWJGLVersion::Create(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(); +} + +const PtrLWJGLVersion LWJGLVersionList::getVersion(const QString &versionName) +{ + for (int i = 0; i < count(); i++) + { + QString name = at(i)->name(); + if ( name == versionName) + return at(i); + } + return PtrLWJGLVersion(); +} + + +void LWJGLVersionList::failed(QString msg) +{ + qWarning() << 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/lists/LwjglVersionList.h b/logic/lists/LwjglVersionList.h new file mode 100644 index 00000000..638a0b67 --- /dev/null +++ b/logic/lists/LwjglVersionList.h @@ -0,0 +1,117 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +class LWJGLVersion; +typedef QSharedPointer PtrLWJGLVersion; + +class LWJGLVersion : public QObject +{ + Q_OBJECT + + LWJGLVersion(const QString &name, const QString &url, QObject *parent = 0) : + QObject(parent), m_name(name), m_url(url) { } +public: + + static PtrLWJGLVersion Create(const QString &name, const QString &url, QObject *parent = 0) + { + return PtrLWJGLVersion(new LWJGLVersion(name, url, parent)); + }; + + QString name() const { return m_name; } + + QString url() const { return m_url; } + +protected: + QString m_name; + QString m_url; +}; + +class LWJGLVersionList : public QAbstractListModel +{ + Q_OBJECT +public: + explicit LWJGLVersionList(QObject *parent = 0); + + static LWJGLVersionList &get(); + + bool isLoaded() { return m_vlist.length() > 0; } + + const PtrLWJGLVersion getVersion(const QString &versionName); + PtrLWJGLVersion at(int index) { return m_vlist[index]; } + const PtrLWJGLVersion at(int index) const { return m_vlist[index]; } + + int count() const { return m_vlist.length(); } + + virtual QVariant data(const QModelIndex &index, int role) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + virtual int rowCount(const QModelIndex &parent) const { return count(); } + virtual int columnCount(const QModelIndex &parent) const; + + virtual bool isLoading() const; + virtual bool errored() const { return m_errored; } + + virtual QString lastErrorMsg() const { return m_lastErrorMsg; } + +public slots: + /*! + * Loads the version list. + * This is done asynchronously. On success, the loadListFinished() signal will + * be emitted. The list model will be reset as well, resulting in the modelReset() + * signal being emitted. Note that the model will be reset before loadListFinished() is emitted. + * If loading the list failed, the loadListFailed(QString msg), + * signal will be emitted. + */ + virtual void loadList(); + +signals: + /*! + * Emitted when the list either starts or finishes loading. + * \param loading Whether or not the list is loading. + */ + void loadingStateUpdated(bool loading); + + void loadListFinished(); + + void loadListFailed(QString msg); + +private: + QList 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/lists/MinecraftVersionList.cpp b/logic/lists/MinecraftVersionList.cpp new file mode 100644 index 00000000..80b4fbc0 --- /dev/null +++ b/logic/lists/MinecraftVersionList.cpp @@ -0,0 +1,300 @@ +/* Copyright 2013 Andrew Okin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MinecraftVersionList.h" +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#define MCVLIST_URLBASE "http://s3.amazonaws.com/Minecraft.Download/versions/" +#define ASSETS_URLBASE "http://assets.minecraft.net/" +#define MCN_URLBASE "http://sonicrules.org/mcnweb.py" + +MinecraftVersionList mcVList; + +MinecraftVersionList::MinecraftVersionList(QObject *parent) : + InstVersionList(parent) +{ + +} + +Task *MinecraftVersionList::getLoadTask() +{ + return new MCVListLoadTask(this); +} + +bool MinecraftVersionList::isLoaded() +{ + return m_loaded; +} + +const InstVersionPtr MinecraftVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int MinecraftVersionList::count() const +{ + return m_vlist.count(); +} + +bool cmpVersions(InstVersionPtr first, InstVersionPtr second) +{ + const InstVersion & left = *first; + const InstVersion & right = *second; + return left > right; +} + +void MinecraftVersionList::sort() +{ + beginResetModel(); + qSort(m_vlist.begin(), m_vlist.end(), cmpVersions); + endResetModel(); +} + +InstVersionPtr MinecraftVersionList::getLatestStable() const +{ + for (int i = 0; i < m_vlist.length(); i++) + { + auto ver = m_vlist.at(i).dynamicCast(); + if (ver->is_latest && !ver->is_snapshot) + { + return m_vlist.at(i); + } + } + return InstVersionPtr(); +} + +MinecraftVersionList &MinecraftVersionList::getMainList() +{ + return mcVList; +} + +void MinecraftVersionList::updateListData(QList versions) +{ + beginResetModel(); + m_vlist = versions; + m_loaded = true; + endResetModel(); + // NOW SORT!! + sort(); +} + +inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname) +{ + QDomNodeList elementList = parent.elementsByTagName(tagname); + if (elementList.count()) + return elementList.at(0).toElement(); + else + return QDomElement(); +} + +inline QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + + +MCVListLoadTask::MCVListLoadTask(MinecraftVersionList *vlist) +{ + m_list = vlist; + m_currentStable = NULL; + vlistReply = nullptr; + legacyWhitelist.insert("1.5.2"); + legacyWhitelist.insert("1.5.1"); + legacyWhitelist.insert("1.5"); + legacyWhitelist.insert("1.4.7"); + legacyWhitelist.insert("1.4.6"); + legacyWhitelist.insert("1.4.5"); + legacyWhitelist.insert("1.4.4"); + legacyWhitelist.insert("1.4.2"); + legacyWhitelist.insert("1.3.2"); + legacyWhitelist.insert("1.3.1"); + legacyWhitelist.insert("1.2.5"); + legacyWhitelist.insert("1.2.4"); + legacyWhitelist.insert("1.2.3"); + legacyWhitelist.insert("1.2.2"); + legacyWhitelist.insert("1.2.1"); + legacyWhitelist.insert("1.1"); + legacyWhitelist.insert("1.0.1"); + legacyWhitelist.insert("1.0"); +} + +MCVListLoadTask::~MCVListLoadTask() +{ +} + +void MCVListLoadTask::executeTask() +{ + setStatus("Loading instance version list..."); + auto & worker = NetWorker::spawn(); + vlistReply = worker.get(QNetworkRequest(QUrl(QString(MCVLIST_URLBASE) + "versions.json"))); + connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded())); +} + + +void MCVListLoadTask::list_downloaded() +{ + if(vlistReply->error() != QNetworkReply::QNetworkReply::NoError) + { + vlistReply->deleteLater(); + emitFailed("Failed to load Minecraft main version list" + vlistReply->errorString()); + return; + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(vlistReply->readAll(), &jsonError); + vlistReply->deleteLater(); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing version list JSON:" + jsonError.errorString()); + return; + } + + if(!jsonDoc.isObject()) + { + emitFailed("Error parsing version list JSON: jsonDoc is not an object"); + return; + } + + QJsonObject root = jsonDoc.object(); + + // Get the ID of the latest release and the latest snapshot. + if(!root.value("latest").isObject()) + { + emitFailed("Error parsing version list JSON: version list is missing 'latest' object"); + return; + } + + QJsonObject latest = root.value("latest").toObject(); + + QString latestReleaseID = latest.value("release").toString(""); + QString latestSnapshotID = latest.value("snapshot").toString(""); + if(latestReleaseID.isEmpty()) + { + emitFailed("Error parsing version list JSON: latest release field is missing"); + return; + } + if(latestSnapshotID.isEmpty()) + { + emitFailed("Error parsing version list JSON: latest snapshot field is missing"); + return; + } + + // Now, get the array of versions. + if(!root.value("versions").isArray()) + { + emitFailed("Error parsing version list JSON: version list object is missing 'versions' array"); + return; + } + QJsonArray versions = root.value("versions").toArray(); + + QList tempList; + for (int i = 0; i < versions.count(); i++) + { + bool is_snapshot = false; + bool is_latest = false; + + // Load the version info. + if(!versions[i].isObject()) + { + //FIXME: log this somewhere + continue; + } + QJsonObject version = versions[i].toObject(); + QString versionID = version.value("id").toString(""); + QString versionTimeStr = version.value("releaseTime").toString(""); + QString versionTypeStr = version.value("type").toString(""); + if(versionID.isEmpty() || versionTimeStr.isEmpty() || versionTypeStr.isEmpty()) + { + //FIXME: log this somewhere + continue; + } + + // Parse the timestamp. + QDateTime versionTime = timeFromS3Time(versionTimeStr); + if(!versionTime.isValid()) + { + //FIXME: log this somewhere + continue; + } + // Parse the type. + MinecraftVersion::VersionType versionType; + // OneSix or Legacy. use filter to determine type + if (versionTypeStr == "release") + { + versionType = legacyWhitelist.contains(versionID)?MinecraftVersion::Legacy:MinecraftVersion::OneSix; + is_latest = (versionID == latestReleaseID); + is_snapshot = false; + } + else if(versionTypeStr == "snapshot") // It's a snapshot... yay + { + versionType = legacyWhitelist.contains(versionID)?MinecraftVersion::Legacy:MinecraftVersion::OneSix; + is_latest = (versionID == latestSnapshotID); + is_snapshot = true; + } + else if(versionTypeStr == "old_alpha") + { + versionType = MinecraftVersion::Nostalgia; + is_latest = false; + is_snapshot = false; + } + else if(versionTypeStr == "old_beta") + { + versionType = MinecraftVersion::Legacy; + is_latest = false; + is_snapshot = false; + } + else + { + //FIXME: log this somewhere + continue; + } + // Get the download URL. + QString dlUrl = QString(MCVLIST_URLBASE) + versionID + "/"; + + // Now, we construct the version object and add it to the list. + QSharedPointer mcVersion(new MinecraftVersion()); + mcVersion->name = mcVersion->descriptor = versionID; + mcVersion->timestamp = versionTime.toMSecsSinceEpoch(); + mcVersion->download_url = dlUrl; + mcVersion->is_latest = is_latest; + mcVersion->is_snapshot = is_snapshot; + mcVersion->type = versionType; + tempList.append(mcVersion); + } + m_list->updateListData(tempList); + + emitSucceeded(); + return; +} + +// FIXME: we should have a local cache of the version list and a local cache of version data +bool MCVListLoadTask::loadFromVList() +{ +} diff --git a/logic/lists/MinecraftVersionList.h b/logic/lists/MinecraftVersionList.h new file mode 100644 index 00000000..0477379f --- /dev/null +++ b/logic/lists/MinecraftVersionList.h @@ -0,0 +1,83 @@ +/* Copyright 2013 Andrew Okin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "InstVersionList.h" +#include "logic/tasks/Task.h" +#include "logic/MinecraftVersion.h" + +class MCVListLoadTask; +class QNetworkReply; + +class MinecraftVersionList : public InstVersionList +{ + Q_OBJECT +public: + friend class MCVListLoadTask; + + explicit MinecraftVersionList(QObject *parent = 0); + + virtual Task *getLoadTask(); + virtual bool isLoaded(); + virtual const InstVersionPtr at(int i) const; + virtual int count() const; + virtual void sort(); + + virtual InstVersionPtr getLatestStable() const; + + /*! + * Gets the main version list instance. + */ + static MinecraftVersionList &getMainList(); + + +protected: + QList m_vlist; + + bool m_loaded; + +protected slots: + virtual void updateListData(QList versions); +}; + +class MCVListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit MCVListLoadTask(MinecraftVersionList *vlist); + ~MCVListLoadTask(); + + virtual void executeTask(); + +protected slots: + void list_downloaded(); + +protected: + //! Loads versions from Mojang's official version list. + bool loadFromVList(); + + QNetworkReply *vlistReply; + MinecraftVersionList *m_list; + MinecraftVersion *m_currentStable; + QSet legacyWhitelist; +}; + diff --git a/logic/net/DownloadJob.cpp b/logic/net/DownloadJob.cpp new file mode 100644 index 00000000..ef842dfd --- /dev/null +++ b/logic/net/DownloadJob.cpp @@ -0,0 +1,138 @@ +#include "DownloadJob.h" +#include "pathutils.h" +#include "NetWorker.h" + +DownloadJob::DownloadJob (QUrl url, + QString target_path, + QString expected_md5 ) + :Job() +{ + m_url = url; + m_target_path = target_path; + m_expected_md5 = expected_md5; + + m_check_md5 = m_expected_md5.size(); + m_save_to_file = m_target_path.size(); + m_status = Job_NotStarted; + m_opened_for_saving = false; +} + +JobPtr DownloadJob::create (QUrl url, + QString target_path, + QString expected_md5 ) +{ + return JobPtr ( new DownloadJob ( url, target_path, expected_md5 ) ); +} + +void DownloadJob::start() +{ + if ( m_save_to_file ) + { + QString filename = m_target_path; + m_output_file.setFileName ( filename ); + // if there already is a file and md5 checking is in effect and it can be opened + if ( m_output_file.exists() && m_output_file.open ( QIODevice::ReadOnly ) ) + { + // check the md5 against the expected one + QString hash = QCryptographicHash::hash ( m_output_file.readAll(), QCryptographicHash::Md5 ).toHex().constData(); + m_output_file.close(); + // skip this file if they match + if ( m_check_md5 && hash == m_expected_md5 ) + { + qDebug() << "Skipping " << m_url.toString() << ": md5 match."; + emit finish(); + return; + } + else + { + m_expected_md5 = hash; + } + } + if(!ensurePathExists(filename)) + { + emit fail(); + return; + } + } + qDebug() << "Downloading " << m_url.toString(); + QNetworkRequest request ( m_url ); + request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1()); + + auto &worker = NetWorker::spawn(); + QNetworkReply * rep = worker.get ( request ); + + m_reply = QSharedPointer ( rep, &QObject::deleteLater ); + 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 DownloadJob::downloadProgress ( qint64 bytesReceived, qint64 bytesTotal ) +{ + emit progress ( bytesReceived, bytesTotal ); +} + +void DownloadJob::downloadError ( QNetworkReply::NetworkError error ) +{ + // error happened during download. + // TODO: log the reason why + m_status = Job_Failed; +} + +void DownloadJob::downloadFinished() +{ + // if the download succeeded + if ( m_status != Job_Failed ) + { + // nothing went wrong... + m_status = Job_Finished; + // save the data to the downloadable if we aren't saving to file + if ( !m_save_to_file ) + { + m_data = m_reply->readAll(); + } + else + { + m_output_file.close(); + } + + //TODO: check md5 here! + m_reply.clear(); + emit finish(); + return; + } + // else the download failed + else + { + if ( m_save_to_file ) + { + m_output_file.close(); + m_output_file.remove(); + } + m_reply.clear(); + emit fail(); + return; + } +} + +void DownloadJob::downloadReadyRead() +{ + if( m_save_to_file ) + { + if(!m_opened_for_saving) + { + if ( !m_output_file.open ( QIODevice::WriteOnly ) ) + { + /* + * Can't open the file... the job failed + */ + m_reply->abort(); + emit fail(); + return; + } + m_opened_for_saving = true; + } + m_output_file.write ( m_reply->readAll() ); + } +} diff --git a/logic/net/DownloadJob.h b/logic/net/DownloadJob.h new file mode 100644 index 00000000..cbde3852 --- /dev/null +++ b/logic/net/DownloadJob.h @@ -0,0 +1,51 @@ +#pragma once +#include "JobQueue.h" +#include + +/** + * A single file for the downloader/cache to process. + */ +class LIBUTIL_EXPORT DownloadJob : public Job +{ + Q_OBJECT +public: + DownloadJob(QUrl url, + QString rel_target_path = QString(), + QString expected_md5 = QString() + ); + static JobPtr create(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString()); +public slots: + virtual void start(); + +private slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);; + void downloadError(QNetworkReply::NetworkError error); + void downloadFinished(); + void downloadReadyRead(); + +public: + /// the network reply + QSharedPointer m_reply; + /// source URL + QUrl m_url; + + /// if true, check the md5sum against a provided md5sum + /// also, if a file exists, perform an md5sum first and don't download only if they don't match + bool m_check_md5; + /// the expected md5 checksum + QString m_expected_md5; + + /// save to file? + bool m_save_to_file; + /// is the saving file already open? + bool m_opened_for_saving; + /// if saving to file, use the one specified in this string + QString m_target_path; + /// this is the output file, if any + QFile m_output_file; + /// if not saving to file, downloaded data is placed here + QByteArray m_data; + + /// The file's status + JobStatus m_status; +}; diff --git a/logic/net/JobQueue.h b/logic/net/JobQueue.h new file mode 100644 index 00000000..26f49307 --- /dev/null +++ b/logic/net/JobQueue.h @@ -0,0 +1,180 @@ +#pragma once +#include +#include "libutil_config.h" + +enum JobStatus +{ + Job_NotStarted, + Job_InProgress, + Job_Finished, + Job_Failed +}; + +class JobList; + +class LIBUTIL_EXPORT Job : public QObject +{ + Q_OBJECT +protected: + explicit Job(): QObject(0){}; +public: + virtual ~Job() {}; +signals: + void finish(); + void fail(); + void progress(qint64 current, qint64 total); +public slots: + virtual void start() = 0; +}; +typedef QSharedPointer JobPtr; + +/** + * A list of jobs, to be processed one by one. + */ +class LIBUTIL_EXPORT JobList : public QObject +{ + friend class JobListQueue; + Q_OBJECT +public: + + JobList() : QObject(0) + { + m_status = Job_NotStarted; + current_job_idx = 0; + } + JobStatus getStatus() + { + return m_status; + } + void add(JobPtr dlable) + { + if(m_status == Job_NotStarted) + m_jobs.append(dlable); + //else there's a bug. TODO: catch the bugs + } + JobPtr getFirstJob() + { + if(m_jobs.size()) + return m_jobs[0]; + else + return JobPtr(); + } + void start() + { + current_job_idx = 0; + auto job = m_jobs[current_job_idx]; + + connect(job.data(), SIGNAL(progress(qint64,qint64)), SLOT(currentJobProgress(qint64,qint64))); + connect(job.data(), SIGNAL(finish()), SLOT(currentJobFinished())); + connect(job.data(), SIGNAL(fail()), SLOT(currentJobFailed())); + job->start(); + emit started(); + } +private slots: + void currentJobFinished() + { + if(current_job_idx == m_jobs.size() - 1) + { + m_status = Job_Finished; + emit finished(); + } + else + { + current_job_idx++; + auto job = m_jobs[current_job_idx]; + connect(job.data(), SIGNAL(progress(qint64,qint64)), SLOT(currentJobProgress(qint64,qint64))); + connect(job.data(), SIGNAL(finish()), SLOT(currentJobFinished())); + connect(job.data(), SIGNAL(fail()), SLOT(currentJobFailed())); + job->start(); + } + } + void currentJobFailed() + { + m_status = Job_Failed; + emit failed(); + } + void currentJobProgress(qint64 current, qint64 total) + { + if(!total) + return; + + int total_jobs = m_jobs.size(); + + if(!total_jobs) + return; + + float job_chunk = 1000.0 / float(total_jobs); + float cur = current; + float tot = total; + float last_chunk = (cur / tot) * job_chunk; + + float list_total = job_chunk * current_job_idx + last_chunk; + emit progress(qint64(list_total), 1000LL); + } +private: + QVector m_jobs; + /// The overall status of this job list + JobStatus m_status; + int current_job_idx; +signals: + void progress(qint64 current, qint64 total); + void started(); + void finished(); + void failed(); +}; +typedef QSharedPointer JobListPtr; + + +/** + * A queue of job lists! The job lists fail or finish as units. + */ +class LIBUTIL_EXPORT JobListQueue : public QObject +{ + Q_OBJECT +public: + JobListQueue(QObject *p = 0): + QObject(p), + currentIndex(0), + is_running(false){} + + void enqueue(JobListPtr job) + { + jobs.enqueue(job); + + // finish or fail, we should catch that and start the next one + connect(job.data(),SIGNAL(finished()), SLOT(startNextJob())); + connect(job.data(),SIGNAL(failed()), SLOT(startNextJob())); + + if(!is_running) + { + QTimer::singleShot(0, this, SLOT(startNextJob())); + } + } + +private slots: + void startNextJob() + { + if (jobs.isEmpty()) + { + currentJobList.clear(); + currentIndex = 0; + is_running = false; + emit finishedAllJobs(); + return; + } + + currentJobList = jobs.dequeue(); + is_running = true; + currentIndex = 0; + currentJobList->start(); + } + +signals: + void finishedAllJobs(); + +private: + JobListPtr currentJobList; + QQueue jobs; + unsigned currentIndex; + bool is_running; +}; diff --git a/logic/net/NetWorker.cpp b/logic/net/NetWorker.cpp new file mode 100644 index 00000000..1eef13d9 --- /dev/null +++ b/logic/net/NetWorker.cpp @@ -0,0 +1,12 @@ +#include "NetWorker.h" +#include + +NetWorker& NetWorker::spawn() +{ + static QThreadStorage storage; + if (!storage.hasLocalData()) + { + storage.setLocalData(new NetWorker()); + } + return *storage.localData(); +} diff --git a/logic/net/NetWorker.h b/logic/net/NetWorker.h new file mode 100644 index 00000000..98374e3b --- /dev/null +++ b/logic/net/NetWorker.h @@ -0,0 +1,20 @@ +/* + _.ooo-._ + .OOOP _ '. + dOOOO (_) \ + OOOOOb | + OOOOOOb. | + OOOOOOOOb | + YOO(_)OOO / + 'OOOOOY _.' + '""""'' +*/ + +#pragma once +#include +class NetWorker : public QNetworkAccessManager +{ + Q_OBJECT +public: + static NetWorker &spawn(); +}; \ No newline at end of file diff --git a/logic/tasks/LoginTask.cpp b/logic/tasks/LoginTask.cpp new file mode 100644 index 00000000..21ac2a5d --- /dev/null +++ b/logic/tasks/LoginTask.cpp @@ -0,0 +1,111 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LoginTask.h" +#include "logic/net/NetWorker.h" + +#include + +#include +#include + +#include +#include + +LoginTask::LoginTask( const UserInfo& uInfo, QObject* parent ) : Task(parent), uInfo(uInfo){} + +void LoginTask::executeTask() +{ + setStatus("Logging in..."); + auto & worker = NetWorker::spawn(); + connect(&worker, SIGNAL(finished(QNetworkReply*)), this, SLOT(processNetReply(QNetworkReply*))); + + QUrl loginURL("https://login.minecraft.net/"); + QNetworkRequest netRequest(loginURL); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QUrlQuery params; + params.addQueryItem("user", uInfo.username); + params.addQueryItem("password", uInfo.password); + params.addQueryItem("version", "13"); + + netReply = worker.post(netRequest, params.query(QUrl::EncodeSpaces).toUtf8()); +} + +void LoginTask::processNetReply(QNetworkReply *reply) +{ + if(netReply != reply) + return; + // Check for errors. + switch (reply->error()) + { + case QNetworkReply::NoError: + { + // Check the response code. + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) + { + QString responseStr(reply->readAll()); + + QStringList strings = responseStr.split(":"); + if (strings.count() >= 4) + { + bool parseSuccess; + qint64 latestVersion = strings[0].toLongLong(&parseSuccess); + if (parseSuccess) + { + // strings[1] is the download ticket. It isn't used anymore. + QString username = strings[2]; + QString sessionID = strings[3]; + + result = {username, sessionID, latestVersion}; + emitSucceeded(); + } + else + { + emitFailed("Failed to parse Minecraft version string."); + } + } + else + { + if (responseStr.toLower() == "bad login") + emitFailed("Invalid username or password."); + else if (responseStr.toLower() == "old version") + emitFailed("Launcher outdated, please update."); + else + emitFailed("Login failed: " + responseStr); + } + } + else if (responseCode == 503) + { + emitFailed("The login servers are currently unavailable. Check http://help.mojang.com/ for more info."); + } + else + { + emitFailed(QString("Login failed: Unknown HTTP error %1 occurred.").arg(QString::number(responseCode))); + } + break; + } + + case QNetworkReply::OperationCanceledError: + emitFailed("Login canceled."); + break; + + default: + emitFailed("Login failed: " + reply->errorString()); + break; + } +} diff --git a/logic/tasks/LoginTask.h b/logic/tasks/LoginTask.h new file mode 100644 index 00000000..bde672b8 --- /dev/null +++ b/logic/tasks/LoginTask.h @@ -0,0 +1,58 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LOGINTASK_H +#define LOGINTASK_H + +#include "Task.h" +#include + +struct UserInfo +{ + QString username; + QString password; +}; + +struct LoginResponse +{ + QString username; + QString sessionID; + qint64 latestVersion; +}; + +class QNetworkReply; + +class LoginTask : public Task +{ + Q_OBJECT +public: + explicit LoginTask(const UserInfo& uInfo, QObject *parent = 0); + LoginResponse getResult() + { + return result; + }; + +protected slots: + void processNetReply(QNetworkReply* reply); + +protected: + void executeTask(); + + LoginResponse result; + QNetworkReply* netReply; + UserInfo uInfo; +}; + +#endif // LOGINTASK_H diff --git a/logic/tasks/Task.cpp b/logic/tasks/Task.cpp new file mode 100644 index 00000000..7c148591 --- /dev/null +++ b/logic/tasks/Task.cpp @@ -0,0 +1,85 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Task.h" + +Task::Task(QObject *parent) : + QObject(parent) +{ + +} + +QString Task::getStatus() const +{ + return status; +} + +void Task::setStatus(const QString &status) +{ + this->status = status; + emitStatusChange(status); +} + +int Task::getProgress() const +{ + return progress; +} + +void Task::setProgress(int progress) +{ + this->progress = progress; + emitProgressChange(progress); +} + +void Task::startTask() +{ + emitStarted(); + executeTask(); +} + +void Task::emitStarted() +{ + running = true; + emit started(); +} + +void Task::emitFailed(QString reason) +{ + running = false; + emit failed(reason); +} + +void Task::emitSucceeded() +{ + running = false; + emit succeeded(); +} + + +bool Task::isRunning() const +{ + return running; +} + + +void Task::emitStatusChange(const QString &status) +{ + emit statusChanged(status); +} + +void Task::emitProgressChange(int progress) +{ + emit progressChanged(progress); +} diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h new file mode 100644 index 00000000..91852b0f --- /dev/null +++ b/logic/tasks/Task.h @@ -0,0 +1,65 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TASK_H +#define TASK_H + +#include +#include + +class Task : public QObject +{ + Q_OBJECT +public: + explicit Task(QObject *parent = 0); + + QString getStatus() const; + int getProgress() const; + bool isRunning() const; + +public slots: + void startTask(); + +protected slots: + void setStatus(const QString& status); + void setProgress(int progress); + +signals: + void started(); + void failed(QString reason); + void succeeded(); + + void statusChanged(Task* task, const QString& status); + void progressChanged(Task* task, int progress); + + void statusChanged(const QString& status); + void progressChanged(int progress); + +protected: + virtual void executeTask() = 0; + + virtual void emitStarted(); + virtual void emitFailed(QString reason); + virtual void emitSucceeded(); + + virtual void emitStatusChange(const QString &status); + virtual void emitProgressChange(int progress); + + QString status; + int progress; + bool running = false; +}; + +#endif // TASK_H -- cgit v1.2.3