summaryrefslogtreecommitdiffstats
path: root/logic
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2013-08-17 13:40:51 +0200
committerPetr Mrázek <peterix@gmail.com>2013-08-17 13:40:51 +0200
commit253067c782955380bbf66ac0475dc954375b1ff4 (patch)
treeca97e231fd3a764256d95b5fc8d08fc25ff72161 /logic
parent77e80665422c4e97e2286418ab55e20c4030023b (diff)
downloadMultiMC-253067c782955380bbf66ac0475dc954375b1ff4.tar
MultiMC-253067c782955380bbf66ac0475dc954375b1ff4.tar.gz
MultiMC-253067c782955380bbf66ac0475dc954375b1ff4.tar.lz
MultiMC-253067c782955380bbf66ac0475dc954375b1ff4.tar.xz
MultiMC-253067c782955380bbf66ac0475dc954375b1ff4.zip
Move all the things (YES. Move them.)
Also, implemented some basic modlist logic, to be wired up.
Diffstat (limited to 'logic')
-rw-r--r--logic/BaseInstance.cpp181
-rw-r--r--logic/BaseInstance.h145
-rw-r--r--logic/BaseInstance_p.h14
-rw-r--r--logic/BaseUpdate.cpp13
-rw-r--r--logic/BaseUpdate.h49
-rw-r--r--logic/CMakeLists.txt24
-rw-r--r--logic/IconListModel.cpp163
-rw-r--r--logic/IconListModel.h33
-rw-r--r--logic/InstanceFactory.cpp113
-rw-r--r--logic/InstanceFactory.h80
-rw-r--r--logic/InstanceVersion.h68
-rw-r--r--logic/LegacyForge.cpp57
-rw-r--r--logic/LegacyForge.h25
-rw-r--r--logic/LegacyInstance.cpp274
-rw-r--r--logic/LegacyInstance.h95
-rw-r--r--logic/LegacyInstance_p.h15
-rw-r--r--logic/LegacyUpdate.cpp381
-rw-r--r--logic/LegacyUpdate.h67
-rw-r--r--logic/MinecraftProcess.cpp169
-rw-r--r--logic/MinecraftProcess.h86
-rw-r--r--logic/MinecraftVersion.h76
-rw-r--r--logic/Mod.cpp264
-rw-r--r--logic/Mod.h71
-rw-r--r--logic/ModList.cpp472
-rw-r--r--logic/ModList.h67
-rw-r--r--logic/NostalgiaInstance.cpp12
-rw-r--r--logic/NostalgiaInstance.h10
-rw-r--r--logic/OneSixAssets.cpp162
-rw-r--r--logic/OneSixAssets.h22
-rw-r--r--logic/OneSixInstance.cpp218
-rw-r--r--logic/OneSixInstance.h34
-rw-r--r--logic/OneSixInstance_p.h9
-rw-r--r--logic/OneSixUpdate.cpp169
-rw-r--r--logic/OneSixUpdate.h54
-rw-r--r--logic/OneSixVersion.cpp132
-rw-r--r--logic/OneSixVersion.h212
-rw-r--r--logic/VersionFactory.cpp195
-rw-r--r--logic/VersionFactory.h24
-rw-r--r--logic/lists/InstVersionList.cpp129
-rw-r--r--logic/lists/InstVersionList.h121
-rw-r--r--logic/lists/InstanceList.cpp232
-rw-r--r--logic/lists/InstanceList.h91
-rw-r--r--logic/lists/LwjglVersionList.cpp206
-rw-r--r--logic/lists/LwjglVersionList.h117
-rw-r--r--logic/lists/MinecraftVersionList.cpp300
-rw-r--r--logic/lists/MinecraftVersionList.h83
-rw-r--r--logic/net/DownloadJob.cpp138
-rw-r--r--logic/net/DownloadJob.h51
-rw-r--r--logic/net/JobQueue.h180
-rw-r--r--logic/net/NetWorker.cpp12
-rw-r--r--logic/net/NetWorker.h20
-rw-r--r--logic/tasks/LoginTask.cpp111
-rw-r--r--logic/tasks/LoginTask.h58
-rw-r--r--logic/tasks/Task.cpp85
-rw-r--r--logic/tasks/Task.h65
55 files changed, 6254 insertions, 0 deletions
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 <QFileInfo>
+
+#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<qint64>();
+}
+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 <QObject>
+#include <QDateTime>
+
+#include <settingsobject.h>
+
+#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<QDialog> createModEditDialog ( QWidget* parent ) = 0;
+signals:
+ /*!
+ * \brief Signal emitted when properties relevant to the instance view change
+ */
+ void propertiesChanged(BaseInstance * inst);
+
+protected:
+ QSharedPointer<BaseInstancePrivate> inst_d;
+};
+
+// pointer for lazy people
+typedef QSharedPointer<BaseInstance> 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 <QString>
+#include <settingsobject.h>
+
+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 <QObject>
+#include <QList>
+#include <QUrl>
+
+#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 <pathutils.h>
+#include <QMap>
+#include <QEventLoop>
+#include <QDir>
+
+#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<QString, int> index;
+ QVector<entry> 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 <QMutex>
+#include <QAbstractListModel>
+#include <QtGui/QIcon>
+
+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 <QDir>
+#include <QFileInfo>
+
+#include "BaseInstance.h"
+#include "LegacyInstance.h"
+#include "OneSixInstance.h"
+#include "NostalgiaInstance.h"
+#include "InstanceVersion.h"
+#include "MinecraftVersion.h"
+
+#include "inifile.h"
+#include <inisettingsobject.h>
+#include <setting.h>
+
+#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<MinecraftVersion>();
+ 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 <QObject>
+#include <QMap>
+#include <QList>
+
+#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 <QSharedPointer>
+
+/*!
+ * 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<InstVersion> 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<wxFFileInputStream> in(new wxFFileInputStream("forge.zip"));
+ wxTempFileOutputStream out("forge.zip");
+ wxTextOutputStream textout(out);
+ wxZipInputStream inzip(*in);
+ wxZipOutputStream outzip(out);
+ std::auto_ptr<wxZipEntry> entry;
+ // preserve metadata
+ outzip.CopyArchiveMetaData(inzip);
+ // copy all entries
+ while (entry.reset(inzip.GetNextEntry()), entry.get() != NULL)
+ if (!outzip.CopyEntry(entry.release(), inzip))
+ return false;
+ // release last entry
+ in.reset();
+ outzip.PutNextEntry("forgeversion.properties");
+
+ wxStringTokenizer tokenizer(newVersion,".");
+ wxString verFile;
+ verFile << wxString("forge.major.number=") << tokenizer.GetNextToken() << "\n";
+ verFile << wxString("forge.minor.number=") << tokenizer.GetNextToken() << "\n";
+ verFile << wxString("forge.revision.number=") << tokenizer.GetNextToken() << "\n";
+ verFile << wxString("forge.build.number=") << tokenizer.GetNextToken() << "\n";
+ auto buf = verFile.ToUTF8();
+ outzip.Write(buf.data(), buf.length());
+ // check if we succeeded
+ return inzip.Eof() && outzip.Close() && out.Commit();
+ }
+ */
+ return true;
+}
diff --git a/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 <setting.h>
+#include <pathutils.h>
+#include <cmdutils.h>
+#include "gui/LegacyModEditDialog.h"
+#include <QFileInfo>
+#include <QDir>
+#include <QImage>
+
+#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<QDialog> (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<qint64>();
+}
+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<ModList> jarModList();
+ QSharedPointer<ModList> coreModList();
+ QSharedPointer<ModList> 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 <QString>
+#include <settingsobject.h>
+#include "BaseInstance_p.h"
+#include "ModList.h"
+#include <QSharedPointer>
+
+class ModList;
+
+struct LegacyInstancePrivate: public BaseInstancePrivate
+{
+ QSharedPointer<ModList> jar_mod_list;
+ QSharedPointer<ModList> core_mod_list;
+ QSharedPointer<ModList> 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 <pathutils.h>
+#include <quazip.h>
+#include <quazipfile.h>
+
+
+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<QNetworkReply> (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<QNetworkCookie> cookies = qvariant_cast<QList<QNetworkCookie>>(reply->header(QNetworkRequest::SetCookieHeader));
+ if(cookies.count() != 0)
+ {
+ //you must tell which cookie goes with which url
+ worker.cookieJar()->setCookiesFromUrl(cookies, QUrl("sourceforge.net"));
+ }
+
+ //here you can check for the 302 or whatever other header i need
+ QVariant newLoc = reply->header(QNetworkRequest::LocationHeader);
+ if(newLoc.isValid())
+ {
+ 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<QNetworkReply> (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<QString> 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<wxZipEntry> 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<wxZipEntry> 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 <QObject>
+#include <QList>
+#include <QUrl>
+
+#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<QNetworkReply> 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<MinecraftVersion> 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 <orochimarufan.x3@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MinecraftProcess.h"
+
+#include <QDataStream>
+#include <QFile>
+#include <QDir>
+//#include <QImage>
+#include <QProcessEnvironment>
+
+#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 <orochimarufan.x3@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <QProcess>
+
+#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 <QStringList>
+
+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 <pathutils.h>
+#include <QDir>
+
+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<wxZipEntry> 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(&currentFile))
+ {
+ do
+ {
+ if (currentFile.EndsWith("mcmod.info"))
+ {
+ infoFile = Path::Combine(modFile.GetFullPath(), currentFile);
+ break;
+ }
+ } while (modDir.GetNext(&currentFile));
+ }
+ }
+
+ 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<std::string>("modid"));
+ modName = wxStr(pt.get<std::string>("name"));
+ modVersion = wxStr(pt.get<std::string>("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<std::string>();
+ 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 <QFileInfo>
+
+class Mod
+{
+public:
+ enum ModType
+ {
+ MOD_UNKNOWN, //!< Indicates an unspecified mod type.
+ MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
+ MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
+ MOD_FOLDER, //!< The mod is in a folder on the filesystem.
+ };
+
+ Mod(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 <pathutils.h>
+
+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(&currentFile))
+ {
+ 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(&currentFile));
+ }
+
+ 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 <QList>
+#include <QString>
+#include <QDir>
+#include <QAbstractListModel>
+
+#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<Mod> 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 <QString>
+#include <QDebug>
+#include <QtXml/QtXml>
+#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<DownloadJob>();
+ 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 <setting.h>
+#include <pathutils.h>
+#include <cmdutils.h>
+#include <JlCompress.h>
+
+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<QString, QString> with)
+{
+ QString result;
+ QRegExp token_regexp("\\$\\{(.+)\\}");
+ token_regexp.setMinimal(true);
+ QStringList list;
+ int tail = 0;
+ int head = 0;
+ while ((head = token_regexp.indexIn(text, head)) != -1)
+ {
+ result.append(text.mid(tail, head-tail));
+ QString key = token_regexp.cap(1);
+ auto iter = with.find(key);
+ if(iter != with.end())
+ {
+ result.append(*iter);
+ }
+ head += token_regexp.matchedLength();
+ tail = head;
+ }
+ result.append(text.mid(tail));
+ return result;
+}
+
+QStringList OneSixInstance::processMinecraftArgs( QString user, QString session )
+{
+ I_D(OneSixInstance);
+ auto version = d->version;
+ QString args_pattern = version->minecraftArguments;
+
+ QMap<QString, QString> 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 <QStringList>
+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<FullVersion> 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<FullVersion> 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 <QtNetwork>
+
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+#include <QDataStream>
+
+#include <QDebug>
+
+#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<MinecraftVersion>();
+ 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<DownloadJob>();
+
+ 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<FullVersion> 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 <QObject>
+#include <QList>
+#include <QUrl>
+#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<MinecraftVersion> 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<QSharedPointer<Library> > FullVersion::getActiveNormalLibs()
+{
+ QList<QSharedPointer<Library> > output;
+ for ( auto lib: libraries )
+ {
+ if (lib->isActive() && !lib->isNative())
+ {
+ output.append(lib);
+ }
+ }
+ return output;
+}
+
+QList<QSharedPointer<Library> > FullVersion::getActiveNativeLibs()
+{
+ QList<QSharedPointer<Library> > 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 <QtCore>
+
+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<OsRule> create(RuleAction result, OpSys system, QString version_regexp)
+ {
+ return QSharedPointer<OsRule> (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<ImplicitRule> create(RuleAction result)
+ {
+ return QSharedPointer<ImplicitRule> (new ImplicitRule(result));
+ }
+};
+
+class Library
+{
+private:
+ // basic values used internally (so far)
+ QString m_name;
+ QString m_base_url;
+ QList<QSharedPointer<Rule> > 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<OpSys, QString> 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<QSharedPointer<Rule> > 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<QSharedPointer<Library> > 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<Rule> rules;
+
+public:
+ FullVersion()
+ {
+ minimumLauncherVersion = 0xDEADBEEF;
+ }
+
+ QList<QSharedPointer<Library> > getActiveNormalLibs();
+ QList<QSharedPointer<Library> > 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<QSharedPointer<Rule> > FullVersionFactory::parse4rules(QJsonObject & baseObj)
+{
+ QList<QSharedPointer<Rule> > rules;
+ auto rulesVal = baseObj.value("rules");
+ if(rulesVal.isArray())
+ {
+ QJsonArray ruleList = rulesVal.toArray();
+ for(auto ruleVal : ruleList)
+ {
+ QSharedPointer<Rule> rule;
+ if(!ruleVal.isObject())
+ continue;
+ auto ruleObj = ruleVal.toObject();
+ auto actionVal = ruleObj.value("action");
+ if(!actionVal.isString())
+ continue;
+ auto action = RuleAction_fromString(actionVal.toString());
+ if(action == Defer)
+ continue;
+
+ auto osVal = ruleObj.value("os");
+ if(!osVal.isObject())
+ {
+ // add a new implicit action rule
+ rules.append(ImplicitRule::create(action));
+ }
+ 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<FullVersion> FullVersionFactory::parse4(QJsonObject root, QSharedPointer<FullVersion> 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> 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<FullVersion> FullVersionFactory::parse(QByteArray data)
+{
+ QSharedPointer<FullVersion> 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<FullVersion>();
+ }
+
+ if(!jsonDoc.isObject())
+ {
+ error_string = "Error reading version file.";
+ m_error = FullVersionFactory::ParseError;
+ return QSharedPointer<FullVersion>();
+ }
+ 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<FullVersion>();
+ }
+}
+
+
+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 <QtCore>
+
+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<FullVersion> parse(QByteArray data);
+private:
+ QSharedPointer<FullVersion> parse4(QJsonObject root, QSharedPointer<FullVersion> product);
+ QList<QSharedPointer<Rule> > 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 <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+#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<InstVersionPtr > 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 <QDir>
+#include <QFile>
+#include <QDirIterator>
+#include <QThread>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#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<QString, QString> & 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<QString, QString> 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<BaseInstance> 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<InstancePtr> 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 <QObject>
+#include <QSharedPointer>
+
+#include "logic/BaseInstance.h"
+
+class BaseInstance;
+
+class InstanceList : public QObject
+{
+ Q_OBJECT
+private:
+ /*!
+ * \brief Get the instance groups
+ */
+ void loadGroupList(QMap<QString, QString> & 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 <QtNetwork>
+
+#include <QtXml>
+
+#include <QRegExp>
+
+#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<PtrLWJGLVersion> tempList;
+
+ for (int i = 0; i < items.length(); i++)
+ {
+ Q_ASSERT_X(items.at(i).isElement(), "load LWJGL list",
+ "XML element isn't an element... wat?");
+
+ QDomElement linkElement = getDomElementByTagName(items.at(i).toElement(), "link");
+ if (linkElement.isNull())
+ {
+ 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 <QObject>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+#include <QUrl>
+
+#include <QNetworkReply>
+
+class LWJGLVersion;
+typedef QSharedPointer<LWJGLVersion> 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<PtrLWJGLVersion> m_vlist;
+
+ QNetworkReply *m_netReply;
+ QNetworkReply *reply;
+
+ bool m_loading;
+ bool m_errored;
+ QString m_lastErrorMsg;
+
+ void failed(QString msg);
+
+ void finished();
+
+ void setLoading(bool loading);
+
+private slots:
+ virtual void netRequestComplete();
+};
+
diff --git a/logic/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 <logic/net/NetWorker.h>
+
+#include <QDebug>
+
+#include <QtXml>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QJsonParseError>
+
+#include <QtAlgorithms>
+
+#include <QtNetwork>
+
+#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<MinecraftVersion>();
+ if (ver->is_latest && !ver->is_snapshot)
+ {
+ return m_vlist.at(i);
+ }
+ }
+ return InstVersionPtr();
+}
+
+MinecraftVersionList &MinecraftVersionList::getMainList()
+{
+ return mcVList;
+}
+
+void MinecraftVersionList::updateListData(QList<InstVersionPtr > 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<InstVersionPtr > 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<MinecraftVersion> 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 <QObject>
+#include <QList>
+#include <QSet>
+#include <QSharedPointer>
+
+#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<InstVersionPtr > m_vlist;
+
+ bool m_loaded;
+
+protected slots:
+ virtual void updateListData(QList<InstVersionPtr > 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<QString> 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<QNetworkReply> ( 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 <QtNetwork>
+
+/**
+ * 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<QNetworkReply> 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 <QtCore>
+#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<Job> 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<JobPtr> 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<JobList> 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<JobListPtr> 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 <QThreadStorage>
+
+NetWorker& NetWorker::spawn()
+{
+ static QThreadStorage<NetWorker *> 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 <QNetworkAccessManager>
+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 <QStringList>
+
+#include <QNetworkReply>
+#include <QNetworkRequest>
+
+#include <QUrl>
+#include <QUrlQuery>
+
+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 <QSharedPointer>
+
+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 <QObject>
+#include <QString>
+
+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