summaryrefslogtreecommitdiffstats
path: root/logic/minecraft
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2016-02-27 19:58:40 +0100
committerPetr Mrázek <peterix@gmail.com>2016-02-27 19:58:40 +0100
commit17ad1e64f824fba6d8f153191effdb2af7d387c8 (patch)
tree3c0996cab5cff0dcf3807afe08df66dda3a50ec4 /logic/minecraft
parent71e4b147ec3b8c5f3e2a004920ffbc0a3b6e18c8 (diff)
downloadMultiMC-17ad1e64f824fba6d8f153191effdb2af7d387c8.tar
MultiMC-17ad1e64f824fba6d8f153191effdb2af7d387c8.tar.gz
MultiMC-17ad1e64f824fba6d8f153191effdb2af7d387c8.tar.lz
MultiMC-17ad1e64f824fba6d8f153191effdb2af7d387c8.tar.xz
MultiMC-17ad1e64f824fba6d8f153191effdb2af7d387c8.zip
NOISSUE move files into paths that make more sense
Diffstat (limited to 'logic/minecraft')
-rw-r--r--logic/minecraft/RawLibrary.h2
-rw-r--r--logic/minecraft/auth/AuthSession.cpp30
-rw-r--r--logic/minecraft/auth/AuthSession.h51
-rw-r--r--logic/minecraft/auth/MojangAccount.cpp278
-rw-r--r--logic/minecraft/auth/MojangAccount.h173
-rw-r--r--logic/minecraft/auth/MojangAccountList.cpp427
-rw-r--r--logic/minecraft/auth/MojangAccountList.h201
-rw-r--r--logic/minecraft/auth/YggdrasilTask.cpp255
-rw-r--r--logic/minecraft/auth/YggdrasilTask.h150
-rw-r--r--logic/minecraft/auth/flows/AuthenticateTask.cpp202
-rw-r--r--logic/minecraft/auth/flows/AuthenticateTask.h46
-rw-r--r--logic/minecraft/auth/flows/RefreshTask.cpp144
-rw-r--r--logic/minecraft/auth/flows/RefreshTask.h44
-rw-r--r--logic/minecraft/auth/flows/ValidateTask.cpp61
-rw-r--r--logic/minecraft/auth/flows/ValidateTask.h47
-rw-r--r--logic/minecraft/forge/ForgeInstaller.cpp442
-rw-r--r--logic/minecraft/forge/ForgeInstaller.h52
-rw-r--r--logic/minecraft/forge/ForgeMirror.h10
-rw-r--r--logic/minecraft/forge/ForgeMirrors.cpp118
-rw-r--r--logic/minecraft/forge/ForgeMirrors.h61
-rw-r--r--logic/minecraft/forge/ForgeVersion.cpp55
-rw-r--r--logic/minecraft/forge/ForgeVersion.h42
-rw-r--r--logic/minecraft/forge/ForgeVersionList.cpp450
-rw-r--r--logic/minecraft/forge/ForgeVersionList.h90
-rw-r--r--logic/minecraft/forge/ForgeXzDownload.cpp389
-rw-r--r--logic/minecraft/forge/ForgeXzDownload.h66
-rw-r--r--logic/minecraft/forge/LegacyForge.cpp56
-rw-r--r--logic/minecraft/forge/LegacyForge.h25
-rw-r--r--logic/minecraft/ftb/FTBPlugin.cpp395
-rw-r--r--logic/minecraft/ftb/FTBPlugin.h13
-rw-r--r--logic/minecraft/ftb/FTBProfileStrategy.cpp128
-rw-r--r--logic/minecraft/ftb/FTBProfileStrategy.h21
-rw-r--r--logic/minecraft/ftb/FTBVersion.h32
-rw-r--r--logic/minecraft/ftb/LegacyFTBInstance.cpp27
-rw-r--r--logic/minecraft/ftb/LegacyFTBInstance.h13
-rw-r--r--logic/minecraft/ftb/OneSixFTBInstance.cpp138
-rw-r--r--logic/minecraft/ftb/OneSixFTBInstance.h27
-rw-r--r--logic/minecraft/legacy/LegacyInstance.cpp (renamed from logic/minecraft/LegacyInstance.cpp)2
-rw-r--r--logic/minecraft/legacy/LegacyInstance.h (renamed from logic/minecraft/LegacyInstance.h)0
-rw-r--r--logic/minecraft/legacy/LegacyUpdate.cpp (renamed from logic/minecraft/LegacyUpdate.cpp)4
-rw-r--r--logic/minecraft/legacy/LegacyUpdate.h (renamed from logic/minecraft/LegacyUpdate.h)0
-rw-r--r--logic/minecraft/legacy/LwjglVersionList.cpp (renamed from logic/minecraft/LwjglVersionList.cpp)0
-rw-r--r--logic/minecraft/legacy/LwjglVersionList.h (renamed from logic/minecraft/LwjglVersionList.h)0
-rw-r--r--logic/minecraft/liteloader/LiteLoaderInstaller.cpp141
-rw-r--r--logic/minecraft/liteloader/LiteLoaderInstaller.h39
-rw-r--r--logic/minecraft/liteloader/LiteLoaderVersionList.cpp275
-rw-r--r--logic/minecraft/liteloader/LiteLoaderVersionList.h119
-rw-r--r--logic/minecraft/onesix/OneSixInstance.cpp (renamed from logic/minecraft/OneSixInstance.cpp)20
-rw-r--r--logic/minecraft/onesix/OneSixInstance.h (renamed from logic/minecraft/OneSixInstance.h)0
-rw-r--r--logic/minecraft/onesix/OneSixProfileStrategy.cpp (renamed from logic/minecraft/OneSixProfileStrategy.cpp)5
-rw-r--r--logic/minecraft/onesix/OneSixProfileStrategy.h (renamed from logic/minecraft/OneSixProfileStrategy.h)2
-rw-r--r--logic/minecraft/onesix/OneSixUpdate.cpp (renamed from logic/minecraft/OneSixUpdate.cpp)4
-rw-r--r--logic/minecraft/onesix/OneSixUpdate.h (renamed from logic/minecraft/OneSixUpdate.h)0
53 files changed, 5353 insertions, 19 deletions
diff --git a/logic/minecraft/RawLibrary.h b/logic/minecraft/RawLibrary.h
index 7f0aed2f..62791239 100644
--- a/logic/minecraft/RawLibrary.h
+++ b/logic/minecraft/RawLibrary.h
@@ -8,7 +8,7 @@
#include <QUrl>
#include <memory>
-#include "minecraft/OneSixRule.h"
+#include "OneSixRule.h"
#include "minecraft/OpSys.h"
#include "GradleSpecifier.h"
#include "net/URLConstants.h"
diff --git a/logic/minecraft/auth/AuthSession.cpp b/logic/minecraft/auth/AuthSession.cpp
new file mode 100644
index 00000000..8758bfbd
--- /dev/null
+++ b/logic/minecraft/auth/AuthSession.cpp
@@ -0,0 +1,30 @@
+#include "AuthSession.h"
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QStringList>
+
+QString AuthSession::serializeUserProperties()
+{
+ QJsonObject userAttrs;
+ for (auto key : u.properties.keys())
+ {
+ auto array = QJsonArray::fromStringList(u.properties.values(key));
+ userAttrs.insert(key, array);
+ }
+ QJsonDocument value(userAttrs);
+ return value.toJson(QJsonDocument::Compact);
+
+}
+
+bool AuthSession::MakeOffline(QString offline_playername)
+{
+ if (status != PlayableOffline && status != PlayableOnline)
+ {
+ return false;
+ }
+ session = "-";
+ player_name = offline_playername;
+ status = PlayableOffline;
+ return true;
+}
diff --git a/logic/minecraft/auth/AuthSession.h b/logic/minecraft/auth/AuthSession.h
new file mode 100644
index 00000000..dede90a9
--- /dev/null
+++ b/logic/minecraft/auth/AuthSession.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include <QString>
+#include <QMultiMap>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+struct User
+{
+ QString id;
+ QMultiMap<QString, QString> properties;
+};
+
+struct MULTIMC_LOGIC_EXPORT AuthSession
+{
+ bool MakeOffline(QString offline_playername);
+
+ QString serializeUserProperties();
+
+ enum Status
+ {
+ Undetermined,
+ RequiresPassword,
+ PlayableOffline,
+ PlayableOnline
+ } status = Undetermined;
+
+ User u;
+
+ // client token
+ QString client_token;
+ // account user name
+ QString username;
+ // combined session ID
+ QString session;
+ // volatile auth token
+ QString access_token;
+ // profile name
+ QString player_name;
+ // profile ID
+ QString uuid;
+ // 'legacy' or 'mojang', depending on account type
+ QString user_type;
+ // Did the auth server reply?
+ bool auth_server_online = false;
+ // Did the user request online mode?
+ bool wants_online = true;
+};
+
+typedef std::shared_ptr<AuthSession> AuthSessionPtr;
diff --git a/logic/minecraft/auth/MojangAccount.cpp b/logic/minecraft/auth/MojangAccount.cpp
new file mode 100644
index 00000000..69a24c09
--- /dev/null
+++ b/logic/minecraft/auth/MojangAccount.cpp
@@ -0,0 +1,278 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <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 "MojangAccount.h"
+#include "flows/RefreshTask.h"
+#include "flows/AuthenticateTask.h"
+
+#include <QUuid>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QRegExp>
+#include <QStringList>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
+{
+ // The JSON object must at least have a username for it to be valid.
+ if (!object.value("username").isString())
+ {
+ qCritical() << "Can't load Mojang account info from JSON object. Username field is "
+ "missing or of the wrong type.";
+ return nullptr;
+ }
+
+ QString username = object.value("username").toString("");
+ QString clientToken = object.value("clientToken").toString("");
+ QString accessToken = object.value("accessToken").toString("");
+
+ QJsonArray profileArray = object.value("profiles").toArray();
+ if (profileArray.size() < 1)
+ {
+ qCritical() << "Can't load Mojang account with username \"" << username
+ << "\". No profiles found.";
+ return nullptr;
+ }
+
+ QList<AccountProfile> profiles;
+ for (QJsonValue profileVal : profileArray)
+ {
+ QJsonObject profileObject = profileVal.toObject();
+ QString id = profileObject.value("id").toString("");
+ QString name = profileObject.value("name").toString("");
+ bool legacy = profileObject.value("legacy").toBool(false);
+ if (id.isEmpty() || name.isEmpty())
+ {
+ qWarning() << "Unable to load a profile because it was missing an ID or a name.";
+ continue;
+ }
+ profiles.append({id, name, legacy});
+ }
+
+ MojangAccountPtr account(new MojangAccount());
+ if (object.value("user").isObject())
+ {
+ User u;
+ QJsonObject userStructure = object.value("user").toObject();
+ u.id = userStructure.value("id").toString();
+ /*
+ QJsonObject propMap = userStructure.value("properties").toObject();
+ for(auto key: propMap.keys())
+ {
+ auto values = propMap.operator[](key).toArray();
+ for(auto value: values)
+ u.properties.insert(key, value.toString());
+ }
+ */
+ account->m_user = u;
+ }
+ account->m_username = username;
+ account->m_clientToken = clientToken;
+ account->m_accessToken = accessToken;
+ account->m_profiles = profiles;
+
+ // Get the currently selected profile.
+ QString currentProfile = object.value("activeProfile").toString("");
+ if (!currentProfile.isEmpty())
+ account->setCurrentProfile(currentProfile);
+
+ return account;
+}
+
+MojangAccountPtr MojangAccount::createFromUsername(const QString &username)
+{
+ MojangAccountPtr account(new MojangAccount());
+ account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
+ account->m_username = username;
+ return account;
+}
+
+QJsonObject MojangAccount::saveToJson() const
+{
+ QJsonObject json;
+ json.insert("username", m_username);
+ json.insert("clientToken", m_clientToken);
+ json.insert("accessToken", m_accessToken);
+
+ QJsonArray profileArray;
+ for (AccountProfile profile : m_profiles)
+ {
+ QJsonObject profileObj;
+ profileObj.insert("id", profile.id);
+ profileObj.insert("name", profile.name);
+ profileObj.insert("legacy", profile.legacy);
+ profileArray.append(profileObj);
+ }
+ json.insert("profiles", profileArray);
+
+ QJsonObject userStructure;
+ {
+ userStructure.insert("id", m_user.id);
+ /*
+ QJsonObject userAttrs;
+ for(auto key: m_user.properties.keys())
+ {
+ auto array = QJsonArray::fromStringList(m_user.properties.values(key));
+ userAttrs.insert(key, array);
+ }
+ userStructure.insert("properties", userAttrs);
+ */
+ }
+ json.insert("user", userStructure);
+
+ if (m_currentProfile != -1)
+ json.insert("activeProfile", currentProfile()->id);
+
+ return json;
+}
+
+bool MojangAccount::setCurrentProfile(const QString &profileId)
+{
+ for (int i = 0; i < m_profiles.length(); i++)
+ {
+ if (m_profiles[i].id == profileId)
+ {
+ m_currentProfile = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+const AccountProfile *MojangAccount::currentProfile() const
+{
+ if (m_currentProfile == -1)
+ return nullptr;
+ return &m_profiles[m_currentProfile];
+}
+
+AccountStatus MojangAccount::accountStatus() const
+{
+ if (m_accessToken.isEmpty())
+ return NotVerified;
+ else
+ return Verified;
+}
+
+std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session,
+ QString password)
+{
+ Q_ASSERT(m_currentTask.get() == nullptr);
+
+ // take care of the true offline status
+ if (accountStatus() == NotVerified && password.isEmpty())
+ {
+ if (session)
+ {
+ session->status = AuthSession::RequiresPassword;
+ fillSession(session);
+ }
+ return nullptr;
+ }
+
+ if (password.isEmpty())
+ {
+ m_currentTask.reset(new RefreshTask(this));
+ }
+ else
+ {
+ m_currentTask.reset(new AuthenticateTask(this, password));
+ }
+ m_currentTask->assignSession(session);
+
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ return m_currentTask;
+}
+
+void MojangAccount::authSucceeded()
+{
+ auto session = m_currentTask->getAssignedSession();
+ if (session)
+ {
+ session->status =
+ session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
+ fillSession(session);
+ session->auth_server_online = true;
+ }
+ m_currentTask.reset();
+ emit changed();
+}
+
+void MojangAccount::authFailed(QString reason)
+{
+ auto session = m_currentTask->getAssignedSession();
+ // This is emitted when the yggdrasil tasks time out or are cancelled.
+ // -> we treat the error as no-op
+ if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
+ {
+ if (session)
+ {
+ session->status = accountStatus() == Verified ? AuthSession::PlayableOffline
+ : AuthSession::RequiresPassword;
+ session->auth_server_online = false;
+ fillSession(session);
+ }
+ }
+ else
+ {
+ m_accessToken = QString();
+ emit changed();
+ if (session)
+ {
+ session->status = AuthSession::RequiresPassword;
+ session->auth_server_online = true;
+ fillSession(session);
+ }
+ }
+ m_currentTask.reset();
+}
+
+void MojangAccount::fillSession(AuthSessionPtr session)
+{
+ // the user name. you have to have an user name
+ session->username = m_username;
+ // volatile auth token
+ session->access_token = m_accessToken;
+ // the semi-permanent client token
+ session->client_token = m_clientToken;
+ if (currentProfile())
+ {
+ // profile name
+ session->player_name = currentProfile()->name;
+ // profile ID
+ session->uuid = currentProfile()->id;
+ // 'legacy' or 'mojang', depending on account type
+ session->user_type = currentProfile()->legacy ? "legacy" : "mojang";
+ if (!session->access_token.isEmpty())
+ {
+ session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id;
+ }
+ else
+ {
+ session->session = "-";
+ }
+ }
+ else
+ {
+ session->player_name = "Player";
+ session->session = "-";
+ }
+ session->u = user();
+}
diff --git a/logic/minecraft/auth/MojangAccount.h b/logic/minecraft/auth/MojangAccount.h
new file mode 100644
index 00000000..2de0c19c
--- /dev/null
+++ b/logic/minecraft/auth/MojangAccount.h
@@ -0,0 +1,173 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QList>
+#include <QJsonObject>
+#include <QPair>
+#include <QMap>
+
+#include <memory>
+#include "AuthSession.h"
+
+#include "multimc_logic_export.h"
+
+class Task;
+class YggdrasilTask;
+class MojangAccount;
+
+typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
+Q_DECLARE_METATYPE(MojangAccountPtr)
+
+/**
+ * A profile within someone's Mojang account.
+ *
+ * Currently, the profile system has not been implemented by Mojang yet,
+ * but we might as well add some things for it in MultiMC right now so
+ * we don't have to rip the code to pieces to add it later.
+ */
+struct AccountProfile
+{
+ QString id;
+ QString name;
+ bool legacy;
+};
+
+enum AccountStatus
+{
+ NotVerified,
+ Verified
+};
+
+/**
+ * Object that stores information about a certain Mojang account.
+ *
+ * Said information may include things such as that account's username, client token, and access
+ * token if the user chose to stay logged in.
+ */
+class MULTIMC_LOGIC_EXPORT MojangAccount : public QObject
+{
+ Q_OBJECT
+public: /* construction */
+ //! Do not copy accounts. ever.
+ explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;
+
+ //! Default constructor
+ explicit MojangAccount(QObject *parent = 0) : QObject(parent) {};
+
+ //! Creates an empty account for the specified user name.
+ static MojangAccountPtr createFromUsername(const QString &username);
+
+ //! Loads a MojangAccount from the given JSON object.
+ static MojangAccountPtr loadFromJson(const QJsonObject &json);
+
+ //! Saves a MojangAccount to a JSON object and returns it.
+ QJsonObject saveToJson() const;
+
+public: /* manipulation */
+ /**
+ * Sets the currently selected profile to the profile with the given ID string.
+ * If profileId is not in the list of available profiles, the function will simply return
+ * false.
+ */
+ bool setCurrentProfile(const QString &profileId);
+
+ /**
+ * Attempt to login. Empty password means we use the token.
+ * If the attempt fails because we already are performing some task, it returns false.
+ */
+ std::shared_ptr<YggdrasilTask> login(AuthSessionPtr session,
+ QString password = QString());
+
+public: /* queries */
+ const QString &username() const
+ {
+ return m_username;
+ }
+
+ const QString &clientToken() const
+ {
+ return m_clientToken;
+ }
+
+ const QString &accessToken() const
+ {
+ return m_accessToken;
+ }
+
+ const QList<AccountProfile> &profiles() const
+ {
+ return m_profiles;
+ }
+
+ const User &user()
+ {
+ return m_user;
+ }
+
+ //! Returns the currently selected profile (if none, returns nullptr)
+ const AccountProfile *currentProfile() const;
+
+ //! Returns whether the account is NotVerified, Verified or Online
+ AccountStatus accountStatus() const;
+
+signals:
+ /**
+ * This signal is emitted when the account changes
+ */
+ void changed();
+
+ // TODO: better signalling for the various possible state changes - especially errors
+
+protected: /* variables */
+ QString m_username;
+
+ // Used to identify the client - the user can have multiple clients for the same account
+ // Think: different launchers, all connecting to the same account/profile
+ QString m_clientToken;
+
+ // Blank if not logged in.
+ QString m_accessToken;
+
+ // Index of the selected profile within the list of available
+ // profiles. -1 if nothing is selected.
+ int m_currentProfile = -1;
+
+ // List of available profiles.
+ QList<AccountProfile> m_profiles;
+
+ // the user structure, whatever it is.
+ User m_user;
+
+ // current task we are executing here
+ std::shared_ptr<YggdrasilTask> m_currentTask;
+
+private
+slots:
+ void authSucceeded();
+ void authFailed(QString reason);
+
+private:
+ void fillSession(AuthSessionPtr session);
+
+public:
+ friend class YggdrasilTask;
+ friend class AuthenticateTask;
+ friend class ValidateTask;
+ friend class RefreshTask;
+};
diff --git a/logic/minecraft/auth/MojangAccountList.cpp b/logic/minecraft/auth/MojangAccountList.cpp
new file mode 100644
index 00000000..26cbc81a
--- /dev/null
+++ b/logic/minecraft/auth/MojangAccountList.cpp
@@ -0,0 +1,427 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MojangAccountList.h"
+#include "MojangAccount.h"
+
+#include <QIODevice>
+#include <QFile>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QDir>
+
+#include <QDebug>
+
+#include <FileSystem.h>
+
+#define ACCOUNT_LIST_FORMAT_VERSION 2
+
+MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+MojangAccountPtr MojangAccountList::findAccount(const QString &username) const
+{
+ for (int i = 0; i < count(); i++)
+ {
+ MojangAccountPtr account = at(i);
+ if (account->username() == username)
+ return account;
+ }
+ return nullptr;
+}
+
+const MojangAccountPtr MojangAccountList::at(int i) const
+{
+ return MojangAccountPtr(m_accounts.at(i));
+}
+
+void MojangAccountList::addAccount(const MojangAccountPtr account)
+{
+ beginResetModel();
+ connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
+ m_accounts.append(account);
+ endResetModel();
+ onListChanged();
+}
+
+void MojangAccountList::removeAccount(const QString &username)
+{
+ beginResetModel();
+ for (auto account : m_accounts)
+ {
+ if (account->username() == username)
+ {
+ m_accounts.removeOne(account);
+ return;
+ }
+ }
+ endResetModel();
+ onListChanged();
+}
+
+void MojangAccountList::removeAccount(QModelIndex index)
+{
+ beginResetModel();
+ m_accounts.removeAt(index.row());
+ endResetModel();
+ onListChanged();
+}
+
+MojangAccountPtr MojangAccountList::activeAccount() const
+{
+ return m_activeAccount;
+}
+
+void MojangAccountList::setActiveAccount(const QString &username)
+{
+ beginResetModel();
+ if (username.isEmpty())
+ {
+ m_activeAccount = nullptr;
+ }
+ else
+ {
+ for (MojangAccountPtr account : m_accounts)
+ {
+ if (account->username() == username)
+ m_activeAccount = account;
+ }
+ }
+ endResetModel();
+ onActiveChanged();
+}
+
+void MojangAccountList::accountChanged()
+{
+ // the list changed. there is no doubt.
+ onListChanged();
+}
+
+void MojangAccountList::onListChanged()
+{
+ if (m_autosave)
+ // TODO: Alert the user if this fails.
+ saveList();
+
+ emit listChanged();
+}
+
+void MojangAccountList::onActiveChanged()
+{
+ if (m_autosave)
+ saveList();
+
+ emit activeAccountChanged();
+}
+
+int MojangAccountList::count() const
+{
+ return m_accounts.count();
+}
+
+QVariant MojangAccountList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ MojangAccountPtr account = at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case NameColumn:
+ return account->username();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return account->username();
+
+ case PointerRole:
+ return qVariantFromValue(account);
+
+ case Qt::CheckStateRole:
+ switch (index.column())
+ {
+ case ActiveColumn:
+ return account == m_activeAccount;
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return tr("Active?");
+
+ case NameColumn:
+ return tr("Name");
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case NameColumn:
+ return tr("The name of the version.");
+
+ default:
+ return QVariant();
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+int MojangAccountList::rowCount(const QModelIndex &parent) const
+{
+ // Return count
+ return count();
+}
+
+int MojangAccountList::columnCount(const QModelIndex &parent) const
+{
+ return 2;
+}
+
+Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
+ {
+ return Qt::NoItemFlags;
+ }
+
+ return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+}
+
+bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
+ {
+ return false;
+ }
+
+ if(role == Qt::CheckStateRole)
+ {
+ if(value == Qt::Checked)
+ {
+ MojangAccountPtr account = this->at(index.row());
+ this->setActiveAccount(account->username());
+ }
+ }
+
+ emit dataChanged(index, index);
+ return true;
+}
+
+void MojangAccountList::updateListData(QList<MojangAccountPtr> versions)
+{
+ beginResetModel();
+ m_accounts = versions;
+ endResetModel();
+}
+
+bool MojangAccountList::loadList(const QString &filePath)
+{
+ QString path = filePath;
+ if (path.isEmpty())
+ path = m_listFilePath;
+ if (path.isEmpty())
+ {
+ qCritical() << "Can't load Mojang account list. No file path given and no default set.";
+ return false;
+ }
+
+ QFile file(path);
+
+ // Try to open the file and fail if we can't.
+ // TODO: We should probably report this error to the user.
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
+ return false;
+ }
+
+ // Read the file and close it.
+ QByteArray jsonData = file.readAll();
+ file.close();
+
+ QJsonParseError parseError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
+
+ // Fail if the JSON is invalid.
+ if (parseError.error != QJsonParseError::NoError)
+ {
+ qCritical() << QString("Failed to parse account list file: %1 at offset %2")
+ .arg(parseError.errorString(), QString::number(parseError.offset))
+ .toUtf8();
+ return false;
+ }
+
+ // Make sure the root is an object.
+ if (!jsonDoc.isObject())
+ {
+ qCritical() << "Invalid account list JSON: Root should be an array.";
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // Make sure the format version matches.
+ if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION)
+ {
+ QString newName = "accounts-old.json";
+ qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to"
+ << newName;
+
+ // Attempt to rename the old version.
+ file.rename(newName);
+ return false;
+ }
+
+ // Now, load the accounts array.
+ beginResetModel();
+ QJsonArray accounts = root.value("accounts").toArray();
+ for (QJsonValue accountVal : accounts)
+ {
+ QJsonObject accountObj = accountVal.toObject();
+ MojangAccountPtr account = MojangAccount::loadFromJson(accountObj);
+ if (account.get() != nullptr)
+ {
+ connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
+ m_accounts.append(account);
+ }
+ else
+ {
+ qWarning() << "Failed to load an account.";
+ }
+ }
+ // Load the active account.
+ m_activeAccount = findAccount(root.value("activeAccount").toString(""));
+ endResetModel();
+ return true;
+}
+
+bool MojangAccountList::saveList(const QString &filePath)
+{
+ QString path(filePath);
+ if (path.isEmpty())
+ path = m_listFilePath;
+ if (path.isEmpty())
+ {
+ qCritical() << "Can't save Mojang account list. No file path given and no default set.";
+ return false;
+ }
+
+ // make sure the parent folder exists
+ if(!FS::ensureFilePathExists(path))
+ return false;
+
+ // make sure the file wasn't overwritten with a folder before (fixes a bug)
+ QFileInfo finfo(path);
+ if(finfo.isDir())
+ {
+ QDir badDir(path);
+ badDir.removeRecursively();
+ }
+
+ qDebug() << "Writing account list to" << path;
+
+ qDebug() << "Building JSON data structure.";
+ // Build the JSON document to write to the list file.
+ QJsonObject root;
+
+ root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION);
+
+ // Build a list of accounts.
+ qDebug() << "Building account array.";
+ QJsonArray accounts;
+ for (MojangAccountPtr account : m_accounts)
+ {
+ QJsonObject accountObj = account->saveToJson();
+ accounts.append(accountObj);
+ }
+
+ // Insert the account list into the root object.
+ root.insert("accounts", accounts);
+
+ if(m_activeAccount)
+ {
+ // Save the active account.
+ root.insert("activeAccount", m_activeAccount->username());
+ }
+
+ // Create a JSON document object to convert our JSON to bytes.
+ QJsonDocument doc(root);
+
+ // Now that we're done building the JSON object, we can write it to the file.
+ qDebug() << "Writing account list to file.";
+ QFile file(path);
+
+ // Try to open the file and fail if we can't.
+ // TODO: We should probably report this error to the user.
+ if (!file.open(QIODevice::WriteOnly))
+ {
+ qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
+ return false;
+ }
+
+ // Write the JSON to the file.
+ file.write(doc.toJson());
+ file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
+ file.close();
+
+ qDebug() << "Saved account list to" << path;
+
+ return true;
+}
+
+void MojangAccountList::setListFilePath(QString path, bool autosave)
+{
+ m_listFilePath = path;
+ m_autosave = autosave;
+}
+
+bool MojangAccountList::anyAccountIsValid()
+{
+ for(auto account:m_accounts)
+ {
+ if(account->accountStatus() != NotVerified)
+ return true;
+ }
+ return false;
+}
diff --git a/logic/minecraft/auth/MojangAccountList.h b/logic/minecraft/auth/MojangAccountList.h
new file mode 100644
index 00000000..c40fa6a3
--- /dev/null
+++ b/logic/minecraft/auth/MojangAccountList.h
@@ -0,0 +1,201 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "MojangAccount.h"
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+#include "multimc_logic_export.h"
+
+/*!
+ * \brief List of available Mojang accounts.
+ * This should be loaded in the background by MultiMC on startup.
+ *
+ * This class also inherits from QAbstractListModel. Methods from that
+ * class determine how this list shows up in a list view. Said methods
+ * all have a default implementation, but they can be overridden by subclasses to
+ * change the behavior of the list.
+ */
+class MULTIMC_LOGIC_EXPORT MojangAccountList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ModelRoles
+ {
+ PointerRole = 0x34B1CB48
+ };
+
+ enum VListColumns
+ {
+ // TODO: Add icon column.
+
+ // First column - Active?
+ ActiveColumn = 0,
+
+ // Second column - Name
+ NameColumn,
+ };
+
+ explicit MojangAccountList(QObject *parent = 0);
+
+ //! Gets the account at the given index.
+ virtual const MojangAccountPtr at(int i) const;
+
+ //! Returns the number of accounts in the list.
+ virtual int count() const;
+
+ //////// List Model Functions ////////
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ virtual int rowCount(const QModelIndex &parent) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
+
+ /*!
+ * Adds a the given Mojang account to the account list.
+ */
+ virtual void addAccount(const MojangAccountPtr account);
+
+ /*!
+ * Removes the mojang account with the given username from the account list.
+ */
+ virtual void removeAccount(const QString &username);
+
+ /*!
+ * Removes the account at the given QModelIndex.
+ */
+ virtual void removeAccount(QModelIndex index);
+
+ /*!
+ * \brief Finds an account by its username.
+ * \param The username of the account to find.
+ * \return A const pointer to the account with the given username. NULL if
+ * one doesn't exist.
+ */
+ virtual MojangAccountPtr findAccount(const QString &username) const;
+
+ /*!
+ * Sets the default path to save the list file to.
+ * If autosave is true, this list will automatically save to the given path whenever it changes.
+ * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
+ * after calling this function to ensure an autosaved change doesn't overwrite the list you intended
+ * to load.
+ */
+ virtual void setListFilePath(QString path, bool autosave = false);
+
+ /*!
+ * \brief Loads the account list from the given file path.
+ * If the given file is an empty string (default), will load from the default account list file.
+ * \return True if successful, otherwise false.
+ */
+ virtual bool loadList(const QString &file = "");
+
+ /*!
+ * \brief Saves the account list to the given file.
+ * If the given file is an empty string (default), will save from the default account list file.
+ * \return True if successful, otherwise false.
+ */
+ virtual bool saveList(const QString &file = "");
+
+ /*!
+ * \brief Gets a pointer to the account that the user has selected as their "active" account.
+ * Which account is active can be overridden on a per-instance basis, but this will return the one that
+ * is set as active globally.
+ * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer.
+ */
+ virtual MojangAccountPtr activeAccount() const;
+
+ /*!
+ * Sets the given account as the current active account.
+ * If the username given is an empty string, sets the active account to nothing.
+ */
+ virtual void setActiveAccount(const QString &username);
+
+ /*!
+ * Returns true if any of the account is at least Validated
+ */
+ bool anyAccountIsValid();
+
+signals:
+ /*!
+ * Signal emitted to indicate that the account list has changed.
+ * This will also fire if the value of an element in the list changes (will be implemented
+ * later).
+ */
+ void listChanged();
+
+ /*!
+ * Signal emitted to indicate that the active account has changed.
+ */
+ void activeAccountChanged();
+
+public
+slots:
+ /**
+ * This is called when one of the accounts changes and the list needs to be updated
+ */
+ void accountChanged();
+
+protected:
+ /*!
+ * Called whenever the list changes.
+ * This emits the listChanged() signal and autosaves the list (if autosave is enabled).
+ */
+ void onListChanged();
+
+ /*!
+ * Called whenever the active account changes.
+ * Emits the activeAccountChanged() signal and autosaves the list if enabled.
+ */
+ void onActiveChanged();
+
+ QList<MojangAccountPtr> m_accounts;
+
+ /*!
+ * Account that is currently active.
+ */
+ MojangAccountPtr m_activeAccount;
+
+ //! Path to the account list file. Empty string if there isn't one.
+ QString m_listFilePath;
+
+ /*!
+ * If true, the account list will automatically save to the account list path when it changes.
+ * Ignored if m_listFilePath is blank.
+ */
+ bool m_autosave = false;
+
+protected
+slots:
+ /*!
+ * Updates this list with the given list of accounts.
+ * This is done by copying each account in the given list and inserting it
+ * into this one.
+ * We need to do this so that we can set the parents of the accounts are set to this
+ * account list. This can't be done in the load task, because the accounts the load
+ * task creates are on the load task's thread and Qt won't allow their parents
+ * to be set to something created on another thread.
+ * To get around that problem, we invoke this method on the GUI thread, which
+ * then copies the accounts and sets their parents correctly.
+ * \param accounts List of accounts whose parents should be set.
+ */
+ virtual void updateListData(QList<MojangAccountPtr> versions);
+};
diff --git a/logic/minecraft/auth/YggdrasilTask.cpp b/logic/minecraft/auth/YggdrasilTask.cpp
new file mode 100644
index 00000000..c6971c9f
--- /dev/null
+++ b/logic/minecraft/auth/YggdrasilTask.cpp
@@ -0,0 +1,255 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "YggdrasilTask.h"
+#include "MojangAccount.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QByteArray>
+
+#include <Env.h>
+
+#include <net/URLConstants.h>
+
+#include <QDebug>
+
+YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
+ : Task(parent), m_account(account)
+{
+ changeState(STATE_CREATED);
+}
+
+void YggdrasilTask::executeTask()
+{
+ changeState(STATE_SENDING_REQUEST);
+
+ // Get the content of the request we're going to send to the server.
+ QJsonDocument doc(getRequestContent());
+
+ auto worker = ENV.qnam();
+ QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint());
+ QNetworkRequest netRequest(reqUrl);
+ netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ QByteArray requestData = doc.toJson();
+ m_netReply = worker->post(netRequest, requestData);
+ connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
+ connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
+ connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
+ connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors);
+ timeout_keeper.setSingleShot(true);
+ timeout_keeper.start(timeout_max);
+ counter.setSingleShot(false);
+ counter.start(time_step);
+ progress(0, timeout_max);
+ connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout);
+ connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat);
+}
+
+void YggdrasilTask::refreshTimers(qint64, qint64)
+{
+ timeout_keeper.stop();
+ timeout_keeper.start(timeout_max);
+ progress(count = 0, timeout_max);
+}
+void YggdrasilTask::heartbeat()
+{
+ count += time_step;
+ progress(count, timeout_max);
+}
+
+bool YggdrasilTask::abort()
+{
+ progress(timeout_max, timeout_max);
+ // TODO: actually use this in a meaningful way
+ m_aborted = YggdrasilTask::BY_USER;
+ m_netReply->abort();
+ return true;
+}
+
+void YggdrasilTask::abortByTimeout()
+{
+ progress(timeout_max, timeout_max);
+ // TODO: actually use this in a meaningful way
+ m_aborted = YggdrasilTask::BY_TIMEOUT;
+ m_netReply->abort();
+}
+
+void YggdrasilTask::sslErrors(QList<QSslError> errors)
+{
+ int i = 1;
+ for (auto error : errors)
+ {
+ qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
+void YggdrasilTask::processReply()
+{
+ changeState(STATE_PROCESSING_RESPONSE);
+
+ switch (m_netReply->error())
+ {
+ case QNetworkReply::NoError:
+ break;
+ case QNetworkReply::TimeoutError:
+ changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
+ return;
+ case QNetworkReply::OperationCanceledError:
+ changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
+ return;
+ case QNetworkReply::SslHandshakeFailedError:
+ changeState(
+ STATE_FAILED_SOFT,
+ tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
+ "<ul>"
+ "<li>You use Windows XP and need to <a "
+ "href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
+ "your root certificates</a></li>"
+ "<li>Some device on your network is interfering with SSL traffic. In that case, "
+ "you have bigger worries than Minecraft not starting.</li>"
+ "<li>Possibly something else. Check the MultiMC log file for details</li>"
+ "</ul>"));
+ return;
+ // used for invalid credentials and similar errors. Fall through.
+ case QNetworkReply::ContentOperationNotPermittedError:
+ break;
+ default:
+ changeState(STATE_FAILED_SOFT,
+ tr("Authentication operation failed due to a network error: %1 (%2)")
+ .arg(m_netReply->errorString()).arg(m_netReply->error()));
+ return;
+ }
+
+ // Try to parse the response regardless of the response code.
+ // Sometimes the auth server will give more information and an error code.
+ QJsonParseError jsonError;
+ QByteArray replyData = m_netReply->readAll();
+ QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
+ // Check the response code.
+ int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (responseCode == 200)
+ {
+ // If the response code was 200, then there shouldn't be an error. Make sure
+ // anyways.
+ // Also, sometimes an empty reply indicates success. If there was no data received,
+ // pass an empty json object to the processResponse function.
+ if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
+ {
+ processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
+ return;
+ }
+ else
+ {
+ changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response "
+ "JSON response: %1 at offset %2.")
+ .arg(jsonError.errorString())
+ .arg(jsonError.offset));
+ qCritical() << replyData;
+ }
+ return;
+ }
+
+ // If the response code was not 200, then Yggdrasil may have given us information
+ // about the error.
+ // If we can parse the response, then get information from it. Otherwise just say
+ // there was an unknown error.
+ if (jsonError.error == QJsonParseError::NoError)
+ {
+ // We were able to parse the server's response. Woo!
+ // Call processError. If a subclass has overridden it then they'll handle their
+ // stuff there.
+ qDebug() << "The request failed, but the server gave us an error message. "
+ "Processing error.";
+ processError(doc.object());
+ }
+ else
+ {
+ // The server didn't say anything regarding the error. Give the user an unknown
+ // error.
+ qDebug()
+ << "The request failed and the server gave no error message. Unknown error.";
+ changeState(STATE_FAILED_SOFT,
+ tr("An unknown error occurred when trying to communicate with the "
+ "authentication server: %1").arg(m_netReply->errorString()));
+ }
+}
+
+void YggdrasilTask::processError(QJsonObject responseData)
+{
+ QJsonValue errorVal = responseData.value("error");
+ QJsonValue errorMessageValue = responseData.value("errorMessage");
+ QJsonValue causeVal = responseData.value("cause");
+
+ if (errorVal.isString() && errorMessageValue.isString())
+ {
+ m_error = std::shared_ptr<Error>(new Error{
+ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")});
+ changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
+ }
+ else
+ {
+ // Error is not in standard format. Don't set m_error and return unknown error.
+ changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
+ }
+}
+
+QString YggdrasilTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case STATE_CREATED:
+ return "Waiting...";
+ case STATE_SENDING_REQUEST:
+ return tr("Sending request to auth servers...");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Processing response from servers...");
+ case STATE_SUCCEEDED:
+ return tr("Authentication task succeeded.");
+ case STATE_FAILED_SOFT:
+ return tr("Failed to contact the authentication server.");
+ case STATE_FAILED_HARD:
+ return tr("Failed to authenticate.");
+ default:
+ return tr("...");
+ }
+}
+
+void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason)
+{
+ m_state = newState;
+ setStatus(getStateMessage());
+ if (newState == STATE_SUCCEEDED)
+ {
+ emitSucceeded();
+ }
+ else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
+ {
+ emitFailed(reason);
+ }
+}
+
+YggdrasilTask::State YggdrasilTask::state()
+{
+ return m_state;
+}
diff --git a/logic/minecraft/auth/YggdrasilTask.h b/logic/minecraft/auth/YggdrasilTask.h
new file mode 100644
index 00000000..c84cfc06
--- /dev/null
+++ b/logic/minecraft/auth/YggdrasilTask.h
@@ -0,0 +1,150 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <tasks/Task.h>
+
+#include <QString>
+#include <QJsonObject>
+#include <QTimer>
+#include <qsslerror.h>
+
+#include "MojangAccount.h"
+
+class QNetworkReply;
+
+/**
+ * A Yggdrasil task is a task that performs an operation on a given mojang account.
+ */
+class YggdrasilTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0);
+
+ /**
+ * assign a session to this task. the session will be filled with required infomration
+ * upon completion
+ */
+ void assignSession(AuthSessionPtr session)
+ {
+ m_session = session;
+ }
+
+ /// get the assigned session for filling with information.
+ AuthSessionPtr getAssignedSession()
+ {
+ return m_session;
+ }
+
+ /**
+ * Class describing a Yggdrasil error response.
+ */
+ struct Error
+ {
+ QString m_errorMessageShort;
+ QString m_errorMessageVerbose;
+ QString m_cause;
+ };
+
+ enum AbortedBy
+ {
+ BY_NOTHING,
+ BY_USER,
+ BY_TIMEOUT
+ } m_aborted = BY_NOTHING;
+
+ /**
+ * Enum for describing the state of the current task.
+ * Used by the getStateMessage function to determine what the status message should be.
+ */
+ enum State
+ {
+ STATE_CREATED,
+ STATE_SENDING_REQUEST,
+ STATE_PROCESSING_RESPONSE,
+ STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
+ STATE_FAILED_HARD, //!< hard failure. auth is invalid
+ STATE_SUCCEEDED
+ } m_state = STATE_CREATED;
+
+protected:
+
+ virtual void executeTask() override;
+
+ /**
+ * Gets the JSON object that will be sent to the authentication server.
+ * Should be overridden by subclasses.
+ */
+ virtual QJsonObject getRequestContent() const = 0;
+
+ /**
+ * Gets the endpoint to POST to.
+ * No leading slash.
+ */
+ virtual QString getEndpoint() const = 0;
+
+ /**
+ * Processes the response received from the server.
+ * If an error occurred, this should emit a failed signal and return false.
+ * If Yggdrasil gave an error response, it should call setError() first, and then return false.
+ * Otherwise, it should return true.
+ * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
+ * an empty QJsonObject.
+ */
+ virtual void processResponse(QJsonObject responseData) = 0;
+
+ /**
+ * Processes an error response received from the server.
+ * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
+ * \returns a QString error message that will be passed to emitFailed.
+ */
+ virtual void processError(QJsonObject responseData);
+
+ /**
+ * Returns the state message for the given state.
+ * Used to set the status message for the task.
+ * Should be overridden by subclasses that want to change messages for a given state.
+ */
+ virtual QString getStateMessage() const;
+
+protected
+slots:
+ void processReply();
+ void refreshTimers(qint64, qint64);
+ void heartbeat();
+ void sslErrors(QList<QSslError>);
+
+ void changeState(State newState, QString reason=QString());
+public
+slots:
+ virtual bool abort() override;
+ void abortByTimeout();
+ State state();
+protected:
+ // FIXME: segfault disaster waiting to happen
+ MojangAccount *m_account = nullptr;
+ QNetworkReply *m_netReply = nullptr;
+ std::shared_ptr<Error> m_error;
+ QTimer timeout_keeper;
+ QTimer counter;
+ int count = 0; // num msec since time reset
+
+ const int timeout_max = 30000;
+ const int time_step = 50;
+
+ AuthSessionPtr m_session;
+};
diff --git a/logic/minecraft/auth/flows/AuthenticateTask.cpp b/logic/minecraft/auth/flows/AuthenticateTask.cpp
new file mode 100644
index 00000000..8d136f0b
--- /dev/null
+++ b/logic/minecraft/auth/flows/AuthenticateTask.cpp
@@ -0,0 +1,202 @@
+
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AuthenticateTask.h"
+#include "../MojangAccount.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+
+#include <QDebug>
+#include <QUuid>
+
+AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
+ QObject *parent)
+ : YggdrasilTask(account, parent), m_password(password)
+{
+}
+
+QJsonObject AuthenticateTask::getRequestContent() const
+{
+ /*
+ * {
+ * "agent": { // optional
+ * "name": "Minecraft", // So far this is the only encountered value
+ * "version": 1 // This number might be increased
+ * // by the vanilla client in the future
+ * },
+ * "username": "mojang account name", // Can be an email address or player name for
+ // unmigrated accounts
+ * "password": "mojang account password",
+ * "clientToken": "client identifier" // optional
+ * "requestUser": true/false // request the user structure
+ * }
+ */
+ QJsonObject req;
+
+ {
+ QJsonObject agent;
+ // C++ makes string literals void* for some stupid reason, so we have to tell it
+ // QString... Thanks Obama.
+ agent.insert("name", QString("Minecraft"));
+ agent.insert("version", 1);
+ req.insert("agent", agent);
+ }
+
+ req.insert("username", m_account->username());
+ req.insert("password", m_password);
+ req.insert("requestUser", true);
+
+ // If we already have a client token, give it to the server.
+ // Otherwise, let the server give us one.
+
+ if(m_account->m_clientToken.isEmpty())
+ {
+ auto uuid = QUuid::createUuid();
+ auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
+ m_account->m_clientToken = uuidString;
+ }
+ req.insert("clientToken", m_account->m_clientToken);
+
+ return req;
+}
+
+void AuthenticateTask::processResponse(QJsonObject responseData)
+{
+ // Read the response data. We need to get the client token, access token, and the selected
+ // profile.
+ qDebug() << "Processing authentication response.";
+ // qDebug() << responseData;
+ // If we already have a client token, make sure the one the server gave us matches our
+ // existing one.
+ qDebug() << "Getting client token.";
+ QString clientToken = responseData.value("clientToken").toString("");
+ if (clientToken.isEmpty())
+ {
+ // Fail if the server gave us an empty client token
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
+ return;
+ }
+ if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
+ return;
+ }
+ // Set the client token.
+ m_account->m_clientToken = clientToken;
+
+ // Now, we set the access token.
+ qDebug() << "Getting access token.";
+ QString accessToken = responseData.value("accessToken").toString("");
+ if (accessToken.isEmpty())
+ {
+ // Fail if the server didn't give us an access token.
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
+ return;
+ }
+ // Set the access token.
+ m_account->m_accessToken = accessToken;
+
+ // Now we load the list of available profiles.
+ // Mojang hasn't yet implemented the profile system,
+ // but we might as well support what's there so we
+ // don't have trouble implementing it later.
+ qDebug() << "Loading profile list.";
+ QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
+ QList<AccountProfile> loadedProfiles;
+ for (auto iter : availableProfiles)
+ {
+ QJsonObject profile = iter.toObject();
+ // Profiles are easy, we just need their ID and name.
+ QString id = profile.value("id").toString("");
+ QString name = profile.value("name").toString("");
+ bool legacy = profile.value("legacy").toBool(false);
+
+ if (id.isEmpty() || name.isEmpty())
+ {
+ // This should never happen, but we might as well
+ // warn about it if it does so we can debug it easily.
+ // You never know when Mojang might do something truly derpy.
+ qWarning() << "Found entry in available profiles list with missing ID or name "
+ "field. Ignoring it.";
+ }
+
+ // Now, add a new AccountProfile entry to the list.
+ loadedProfiles.append({id, name, legacy});
+ }
+ // Put the list of profiles we loaded into the MojangAccount object.
+ m_account->m_profiles = loadedProfiles;
+
+ // Finally, we set the current profile to the correct value. This is pretty simple.
+ // We do need to make sure that the current profile that the server gave us
+ // is actually in the available profiles list.
+ // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
+ qDebug() << "Setting current profile.";
+ QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
+ QString currentProfileId = currentProfile.value("id").toString("");
+ if (currentProfileId.isEmpty())
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
+ return;
+ }
+ if (!m_account->setCurrentProfile(currentProfileId))
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
+ return;
+ }
+
+ // this is what the vanilla launcher passes to the userProperties launch param
+ if (responseData.contains("user"))
+ {
+ User u;
+ auto obj = responseData.value("user").toObject();
+ u.id = obj.value("id").toString();
+ auto propArray = obj.value("properties").toArray();
+ for (auto prop : propArray)
+ {
+ auto propTuple = prop.toObject();
+ auto name = propTuple.value("name").toString();
+ auto value = propTuple.value("value").toString();
+ u.properties.insert(name, value);
+ }
+ m_account->m_user = u;
+ }
+
+ // We've made it through the minefield of possible errors. Return true to indicate that
+ // we've succeeded.
+ qDebug() << "Finished reading authentication response.";
+ changeState(STATE_SUCCEEDED);
+}
+
+QString AuthenticateTask::getEndpoint() const
+{
+ return "authenticate";
+}
+
+QString AuthenticateTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case STATE_SENDING_REQUEST:
+ return tr("Authenticating: Sending request...");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Authenticating: Processing response...");
+ default:
+ return YggdrasilTask::getStateMessage();
+ }
+}
diff --git a/logic/minecraft/auth/flows/AuthenticateTask.h b/logic/minecraft/auth/flows/AuthenticateTask.h
new file mode 100644
index 00000000..398fab98
--- /dev/null
+++ b/logic/minecraft/auth/flows/AuthenticateTask.h
@@ -0,0 +1,46 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "../YggdrasilTask.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+
+/**
+ * The authenticate task takes a MojangAccount with no access token and password and attempts to
+ * authenticate with Mojang's servers.
+ * If successful, it will set the MojangAccount's access token.
+ */
+class AuthenticateTask : public YggdrasilTask
+{
+ Q_OBJECT
+public:
+ AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
+
+protected:
+ virtual QJsonObject getRequestContent() const override;
+
+ virtual QString getEndpoint() const override;
+
+ virtual void processResponse(QJsonObject responseData) override;
+
+ virtual QString getStateMessage() const override;
+
+private:
+ QString m_password;
+};
diff --git a/logic/minecraft/auth/flows/RefreshTask.cpp b/logic/minecraft/auth/flows/RefreshTask.cpp
new file mode 100644
index 00000000..a0fb2e48
--- /dev/null
+++ b/logic/minecraft/auth/flows/RefreshTask.cpp
@@ -0,0 +1,144 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "RefreshTask.h"
+#include "../MojangAccount.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+
+#include <QDebug>
+
+RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
+{
+}
+
+QJsonObject RefreshTask::getRequestContent() const
+{
+ /*
+ * {
+ * "clientToken": "client identifier"
+ * "accessToken": "current access token to be refreshed"
+ * "selectedProfile": // specifying this causes errors
+ * {
+ * "id": "profile ID"
+ * "name": "profile name"
+ * }
+ * "requestUser": true/false // request the user structure
+ * }
+ */
+ QJsonObject req;
+ req.insert("clientToken", m_account->m_clientToken);
+ req.insert("accessToken", m_account->m_accessToken);
+ /*
+ {
+ auto currentProfile = m_account->currentProfile();
+ QJsonObject profile;
+ profile.insert("id", currentProfile->id());
+ profile.insert("name", currentProfile->name());
+ req.insert("selectedProfile", profile);
+ }
+ */
+ req.insert("requestUser", true);
+
+ return req;
+}
+
+void RefreshTask::processResponse(QJsonObject responseData)
+{
+ // Read the response data. We need to get the client token, access token, and the selected
+ // profile.
+ qDebug() << "Processing authentication response.";
+
+ // qDebug() << responseData;
+ // If we already have a client token, make sure the one the server gave us matches our
+ // existing one.
+ QString clientToken = responseData.value("clientToken").toString("");
+ if (clientToken.isEmpty())
+ {
+ // Fail if the server gave us an empty client token
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
+ return;
+ }
+ if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
+ return;
+ }
+
+ // Now, we set the access token.
+ qDebug() << "Getting new access token.";
+ QString accessToken = responseData.value("accessToken").toString("");
+ if (accessToken.isEmpty())
+ {
+ // Fail if the server didn't give us an access token.
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
+ return;
+ }
+
+ // we validate that the server responded right. (our current profile = returned current
+ // profile)
+ QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
+ QString currentProfileId = currentProfile.value("id").toString("");
+ if (m_account->currentProfile()->id != currentProfileId)
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
+ return;
+ }
+
+ // this is what the vanilla launcher passes to the userProperties launch param
+ if (responseData.contains("user"))
+ {
+ User u;
+ auto obj = responseData.value("user").toObject();
+ u.id = obj.value("id").toString();
+ auto propArray = obj.value("properties").toArray();
+ for (auto prop : propArray)
+ {
+ auto propTuple = prop.toObject();
+ auto name = propTuple.value("name").toString();
+ auto value = propTuple.value("value").toString();
+ u.properties.insert(name, value);
+ }
+ m_account->m_user = u;
+ }
+
+ // We've made it through the minefield of possible errors. Return true to indicate that
+ // we've succeeded.
+ qDebug() << "Finished reading refresh response.";
+ // Reset the access token.
+ m_account->m_accessToken = accessToken;
+ changeState(STATE_SUCCEEDED);
+}
+
+QString RefreshTask::getEndpoint() const
+{
+ return "refresh";
+}
+
+QString RefreshTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case STATE_SENDING_REQUEST:
+ return tr("Refreshing login token...");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Refreshing login token: Processing response...");
+ default:
+ return YggdrasilTask::getStateMessage();
+ }
+}
diff --git a/logic/minecraft/auth/flows/RefreshTask.h b/logic/minecraft/auth/flows/RefreshTask.h
new file mode 100644
index 00000000..17714b4f
--- /dev/null
+++ b/logic/minecraft/auth/flows/RefreshTask.h
@@ -0,0 +1,44 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "../YggdrasilTask.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+
+/**
+ * The authenticate task takes a MojangAccount with a possibly timed-out access token
+ * and attempts to authenticate with Mojang's servers.
+ * If successful, it will set the new access token. The token is considered validated.
+ */
+class RefreshTask : public YggdrasilTask
+{
+ Q_OBJECT
+public:
+ RefreshTask(MojangAccount * account);
+
+protected:
+ virtual QJsonObject getRequestContent() const override;
+
+ virtual QString getEndpoint() const override;
+
+ virtual void processResponse(QJsonObject responseData) override;
+
+ virtual QString getStateMessage() const override;
+};
+
diff --git a/logic/minecraft/auth/flows/ValidateTask.cpp b/logic/minecraft/auth/flows/ValidateTask.cpp
new file mode 100644
index 00000000..4deceb6a
--- /dev/null
+++ b/logic/minecraft/auth/flows/ValidateTask.cpp
@@ -0,0 +1,61 @@
+
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ValidateTask.h"
+#include "../MojangAccount.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+
+#include <QDebug>
+
+ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
+ : YggdrasilTask(account, parent)
+{
+}
+
+QJsonObject ValidateTask::getRequestContent() const
+{
+ QJsonObject req;
+ req.insert("accessToken", m_account->m_accessToken);
+ return req;
+}
+
+void ValidateTask::processResponse(QJsonObject responseData)
+{
+ // Assume that if processError wasn't called, then the request was successful.
+ changeState(YggdrasilTask::STATE_SUCCEEDED);
+}
+
+QString ValidateTask::getEndpoint() const
+{
+ return "validate";
+}
+
+QString ValidateTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case YggdrasilTask::STATE_SENDING_REQUEST:
+ return tr("Validating access token: Sending request...");
+ case YggdrasilTask::STATE_PROCESSING_RESPONSE:
+ return tr("Validating access token: Processing response...");
+ default:
+ return YggdrasilTask::getStateMessage();
+ }
+}
diff --git a/logic/minecraft/auth/flows/ValidateTask.h b/logic/minecraft/auth/flows/ValidateTask.h
new file mode 100644
index 00000000..77d628a0
--- /dev/null
+++ b/logic/minecraft/auth/flows/ValidateTask.h
@@ -0,0 +1,47 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
+ */
+
+#pragma once
+
+#include "../YggdrasilTask.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+
+/**
+ * The validate task takes a MojangAccount and checks to make sure its access token is valid.
+ */
+class ValidateTask : public YggdrasilTask
+{
+ Q_OBJECT
+public:
+ ValidateTask(MojangAccount *account, QObject *parent = 0);
+
+protected:
+ virtual QJsonObject getRequestContent() const override;
+
+ virtual QString getEndpoint() const override;
+
+ virtual void processResponse(QJsonObject responseData) override;
+
+ virtual QString getStateMessage() const override;
+
+private:
+};
diff --git a/logic/minecraft/forge/ForgeInstaller.cpp b/logic/minecraft/forge/ForgeInstaller.cpp
new file mode 100644
index 00000000..a9283e5d
--- /dev/null
+++ b/logic/minecraft/forge/ForgeInstaller.cpp
@@ -0,0 +1,442 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ForgeInstaller.h"
+#include "ForgeVersionList.h"
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/GradleSpecifier.h"
+#include "net/HttpMetaCache.h"
+#include "tasks/Task.h"
+#include "minecraft/onesix/OneSixInstance.h"
+#include "minecraft/VersionFilterData.h"
+#include "Env.h"
+#include "Exception.h"
+#include <FileSystem.h>
+
+#include <quazip.h>
+#include <quazipfile.h>
+#include <QStringList>
+#include <QRegularExpression>
+#include <QRegularExpressionMatch>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QSaveFile>
+#include <QCryptographicHash>
+
+ForgeInstaller::ForgeInstaller() : BaseInstaller()
+{
+}
+
+void ForgeInstaller::prepare(const QString &filename, const QString &universalUrl)
+{
+ std::shared_ptr<MinecraftProfile> newVersion;
+ m_universal_url = universalUrl;
+
+ QuaZip zip(filename);
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ // read the install profile
+ if (!zip.setCurrentFile("install_profile.json"))
+ return;
+
+ QJsonParseError jsonError;
+ if (!file.open(QIODevice::ReadOnly))
+ return;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &jsonError);
+ file.close();
+ if (jsonError.error != QJsonParseError::NoError)
+ return;
+
+ if (!jsonDoc.isObject())
+ return;
+
+ QJsonObject root = jsonDoc.object();
+
+ auto installVal = root.value("install");
+ auto versionInfoVal = root.value("versionInfo");
+ if (!installVal.isObject() || !versionInfoVal.isObject())
+ return;
+
+ // read the forge version info
+ {
+ newVersion = MinecraftProfile::fromJson(versionInfoVal.toObject());
+ if (!newVersion)
+ return;
+ }
+
+ QJsonObject installObj = installVal.toObject();
+ QString libraryName = installObj.value("path").toString();
+ internalPath = installObj.value("filePath").toString();
+ m_forgeVersionString = installObj.value("version").toString().remove("Forge").trimmed();
+
+ // where do we put the library? decode the mojang path
+ GradleSpecifier lib(libraryName);
+
+ auto cacheentry = ENV.metacache()->resolveEntry("libraries", lib.toPath());
+ finalPath = "libraries/" + lib.toPath();
+ if (!FS::ensureFilePathExists(finalPath))
+ return;
+
+ if (!zip.setCurrentFile(internalPath))
+ return;
+ if (!file.open(QIODevice::ReadOnly))
+ return;
+ {
+ QByteArray data = file.readAll();
+ // extract file
+ QSaveFile extraction(finalPath);
+ if (!extraction.open(QIODevice::WriteOnly))
+ return;
+ if (extraction.write(data) != data.size())
+ return;
+ if (!extraction.commit())
+ return;
+ QCryptographicHash md5sum(QCryptographicHash::Md5);
+ md5sum.addData(data);
+
+ cacheentry->stale = false;
+ cacheentry->md5sum = md5sum.result().toHex().constData();
+ ENV.metacache()->updateEntry(cacheentry);
+ }
+ file.close();
+
+ m_forge_json = newVersion;
+ m_forge_json->id = installObj.value("minecraft").toString();
+}
+
+bool ForgeInstaller::add(OneSixInstance *to)
+{
+ if (!BaseInstaller::add(to))
+ {
+ return false;
+ }
+
+ QJsonObject obj;
+ obj.insert("order", 5);
+
+ if (!m_forge_json)
+ return false;
+ int sliding_insert_window = 0;
+ {
+ QJsonArray librariesPlus;
+ // A blacklist
+ QSet<QString> blacklist{"authlib", "realms"};
+ //
+ QList<QString> xzlist{"org.scala-lang", "com.typesafe"};
+ // for each library in the version we are adding (except for the blacklisted)
+ for (auto lib : m_forge_json->libraries)
+ {
+ QString libName = lib->artifactId();
+ QString rawName = lib->rawName();
+
+ // ignore lwjgl libraries.
+ if (g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix()))
+ continue;
+ // ignore other blacklisted (realms, authlib)
+ if (blacklist.contains(libName))
+ continue;
+
+ // WARNING: This could actually break.
+ // if this is the actual forge lib, set an absolute url for the download
+ if (m_forge_version->type == ForgeVersion::Gradle)
+ {
+ if (libName == "forge")
+ {
+ lib->setClassifier("universal");
+ }
+ else if (libName == "minecraftforge")
+ {
+ QString forgeCoord("net.minecraftforge:forge:%1:universal");
+ // using insane form of the MC version...
+ QString longVersion =
+ m_forge_version->mcver + "-" + m_forge_version->jobbuildver;
+ GradleSpecifier spec(forgeCoord.arg(longVersion));
+ lib->setRawName(spec);
+ }
+ }
+ else
+ {
+ if (libName.contains("minecraftforge"))
+ {
+ lib->setAbsoluteUrl(m_universal_url);
+ }
+ }
+
+ // WARNING: This could actually break.
+ // mark bad libraries based on the xzlist above
+ for (auto entry : xzlist)
+ {
+ qDebug() << "Testing " << rawName << " : " << entry;
+ if (rawName.startsWith(entry))
+ {
+ lib->setHint("forge-pack-xz");
+ break;
+ }
+ }
+
+ QJsonObject libObj = lib->toJson();
+
+ bool found = false;
+ bool equals = false;
+ // find an entry that matches this one
+ for (auto tolib : to->getMinecraftProfile()->vanillaLibraries)
+ {
+ if (tolib->artifactId() != libName)
+ continue;
+ found = true;
+ if (tolib->toJson() == libObj)
+ {
+ equals = true;
+ }
+ // replace lib
+ libObj.insert("insert", QString("replace"));
+ break;
+ }
+ if (equals)
+ {
+ continue;
+ }
+ if (!found)
+ {
+ // add lib
+ libObj.insert("insert", QString("prepend"));
+ if (lib->artifactId() == "minecraftforge" || lib->artifactId() == "forge")
+ {
+ libObj.insert("MMC-depend", QString("hard"));
+ }
+ sliding_insert_window++;
+ }
+ librariesPlus.prepend(libObj);
+ }
+ obj.insert("+libraries", librariesPlus);
+ obj.insert("mainClass", m_forge_json->mainClass);
+ QString args = m_forge_json->minecraftArguments;
+ QStringList tweakers;
+ {
+ QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)");
+ QRegularExpressionMatch match = expression.match(args);
+ while (match.hasMatch())
+ {
+ tweakers.append(match.captured(1));
+ args.remove(match.capturedStart(), match.capturedLength());
+ match = expression.match(args);
+ }
+ }
+ if (!args.isEmpty() && args != to->getMinecraftProfile()->vanillaMinecraftArguments)
+ {
+ obj.insert("minecraftArguments", args);
+ }
+ if (!tweakers.isEmpty())
+ {
+ obj.insert("+tweakers", QJsonArray::fromStringList(tweakers));
+ }
+ if (!m_forge_json->processArguments.isEmpty() &&
+ m_forge_json->processArguments != to->getMinecraftProfile()->vanillaProcessArguments)
+ {
+ obj.insert("processArguments", m_forge_json->processArguments);
+ }
+ }
+
+ obj.insert("name", QString("Forge"));
+ obj.insert("fileId", id());
+ obj.insert("version", m_forgeVersionString);
+ obj.insert("mcVersion", to->intendedVersionId());
+
+ QFile file(filename(to->instanceRoot()));
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(QJsonDocument(obj).toJson());
+ file.close();
+
+ return true;
+}
+
+bool ForgeInstaller::addLegacy(OneSixInstance *to)
+{
+ if (!BaseInstaller::add(to))
+ {
+ return false;
+ }
+ auto entry = ENV.metacache()->resolveEntry("minecraftforge", m_forge_version->filename());
+ finalPath = FS::PathCombine(to->jarModsDir(), m_forge_version->filename());
+ if (!FS::ensureFilePathExists(finalPath))
+ {
+ return false;
+ }
+ if (!QFile::copy(entry->getFullPath(), finalPath))
+ {
+ return false;
+ }
+ QJsonObject obj;
+ obj.insert("order", 5);
+ {
+ QJsonArray jarmodsPlus;
+ {
+ QJsonObject libObj;
+ libObj.insert("name", m_forge_version->universal_filename);
+ jarmodsPlus.append(libObj);
+ }
+ obj.insert("+jarMods", jarmodsPlus);
+ }
+
+ obj.insert("name", QString("Forge"));
+ obj.insert("fileId", id());
+ obj.insert("version", m_forge_version->jobbuildver);
+ obj.insert("mcVersion", to->intendedVersionId());
+ if (g_VersionFilterData.fmlLibsMapping.contains(m_forge_version->mcver))
+ {
+ QJsonArray traitsPlus;
+ traitsPlus.append(QString("legacyFML"));
+ obj.insert("+traits", traitsPlus);
+ }
+ auto fullversion = to->getMinecraftProfile();
+ fullversion->remove("net.minecraftforge");
+
+ QFile file(filename(to->instanceRoot()));
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(QJsonDocument(obj).toJson());
+ file.close();
+ return true;
+}
+
+class ForgeInstallTask : public Task
+{
+ Q_OBJECT
+public:
+ ForgeInstallTask(ForgeInstaller *installer, OneSixInstance *instance,
+ BaseVersionPtr version, QObject *parent = 0)
+ : Task(parent), m_installer(installer), m_instance(instance), m_version(version)
+ {
+ }
+
+protected:
+ void executeTask() override
+ {
+ setStatus(tr("Installing Forge..."));
+ ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast<ForgeVersion>(m_version);
+ if (!forgeVersion)
+ {
+ emitFailed(tr("Unknown error occured"));
+ return;
+ }
+ prepare(forgeVersion);
+ }
+ void prepare(ForgeVersionPtr forgeVersion)
+ {
+ auto entry = ENV.metacache()->resolveEntry("minecraftforge", forgeVersion->filename());
+ auto installFunction = [this, entry, forgeVersion]()
+ {
+ if (!install(entry, forgeVersion))
+ {
+ qCritical() << "Failure installing Forge";
+ emitFailed(tr("Failure to install Forge"));
+ }
+ else
+ {
+ reload();
+ }
+ };
+
+ /*
+ * HACK IF the local non-stale file is too small, mark is as stale
+ *
+ * This fixes some problems with bad files acquired because of unhandled HTTP redirects
+ * in old versions of MultiMC.
+ */
+ if (!entry->stale)
+ {
+ QFileInfo localFile(entry->getFullPath());
+ if (localFile.size() <= 0x4000)
+ {
+ entry->stale = true;
+ }
+ }
+
+ if (entry->stale)
+ {
+ NetJob *fjob = new NetJob("Forge download");
+ fjob->addNetAction(CacheDownload::make(forgeVersion->url(), entry));
+ connect(fjob, &NetJob::progress, this, &Task::setProgress);
+ connect(fjob, &NetJob::status, this, &Task::setStatus);
+ connect(fjob, &NetJob::failed, [this](QString reason)
+ { emitFailed(tr("Failure to download Forge:\n%1").arg(reason)); });
+ connect(fjob, &NetJob::succeeded, installFunction);
+ fjob->start();
+ }
+ else
+ {
+ installFunction();
+ }
+ }
+ bool install(const std::shared_ptr<MetaEntry> &entry, const ForgeVersionPtr &forgeVersion)
+ {
+ if (forgeVersion->usesInstaller())
+ {
+ QString forgePath = entry->getFullPath();
+ m_installer->prepare(forgePath, forgeVersion->universal_url);
+ return m_installer->add(m_instance);
+ }
+ else
+ return m_installer->addLegacy(m_instance);
+ }
+ void reload()
+ {
+ try
+ {
+ m_instance->reloadProfile();
+ emitSucceeded();
+ }
+ catch (Exception &e)
+ {
+ emitFailed(e.cause());
+ }
+ catch (...)
+ {
+ emitFailed(tr("Failed to load the version description file for reasons unknown."));
+ }
+ }
+
+private:
+ ForgeInstaller *m_installer;
+ OneSixInstance *m_instance;
+ BaseVersionPtr m_version;
+};
+
+Task *ForgeInstaller::createInstallTask(OneSixInstance *instance,
+ BaseVersionPtr version, QObject *parent)
+{
+ if (!version)
+ {
+ return nullptr;
+ }
+ m_forge_version = std::dynamic_pointer_cast<ForgeVersion>(version);
+ return new ForgeInstallTask(this, instance, version, parent);
+}
+
+#include "ForgeInstaller.moc"
diff --git a/logic/minecraft/forge/ForgeInstaller.h b/logic/minecraft/forge/ForgeInstaller.h
new file mode 100644
index 00000000..0de762b6
--- /dev/null
+++ b/logic/minecraft/forge/ForgeInstaller.h
@@ -0,0 +1,52 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "BaseInstaller.h"
+
+#include <QString>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class MinecraftProfile;
+class ForgeInstallTask;
+struct ForgeVersion;
+
+class MULTIMC_LOGIC_EXPORT ForgeInstaller : public BaseInstaller
+{
+ friend class ForgeInstallTask;
+public:
+ ForgeInstaller();
+ virtual ~ForgeInstaller(){}
+ virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override;
+ virtual QString id() const override { return "net.minecraftforge"; }
+
+protected:
+ void prepare(const QString &filename, const QString &universalUrl);
+ bool add(OneSixInstance *to) override;
+ bool addLegacy(OneSixInstance *to);
+
+private:
+ // the parsed version json, read from the installer
+ std::shared_ptr<MinecraftProfile> m_forge_json;
+ // the actual forge version
+ std::shared_ptr<ForgeVersion> m_forge_version;
+ QString internalPath;
+ QString finalPath;
+ QString m_forgeVersionString;
+ QString m_universal_url;
+};
diff --git a/logic/minecraft/forge/ForgeMirror.h b/logic/minecraft/forge/ForgeMirror.h
new file mode 100644
index 00000000..2518dffe
--- /dev/null
+++ b/logic/minecraft/forge/ForgeMirror.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <QString>
+
+struct ForgeMirror
+{
+ QString name;
+ QString logo_url;
+ QString website_url;
+ QString mirror_url;
+}; \ No newline at end of file
diff --git a/logic/minecraft/forge/ForgeMirrors.cpp b/logic/minecraft/forge/ForgeMirrors.cpp
new file mode 100644
index 00000000..a2fc2c62
--- /dev/null
+++ b/logic/minecraft/forge/ForgeMirrors.cpp
@@ -0,0 +1,118 @@
+#include "Env.h"
+#include "ForgeMirrors.h"
+#include <QDebug>
+#include <algorithm>
+#include <random>
+
+ForgeMirrors::ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
+ QString mirrorlist)
+{
+ m_libs = libs;
+ m_parent_job = parent_job;
+ m_url = QUrl(mirrorlist);
+ m_status = Job_NotStarted;
+}
+
+void ForgeMirrors::start()
+{
+ qDebug() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void ForgeMirrors::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Error getting URL:" << m_url.toString().toLocal8Bit()
+ << "Network error: " << error;
+ m_status = Job_Failed;
+}
+
+void ForgeMirrors::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong... ?
+ parseMirrorList();
+ return;
+ }
+ // else the download failed, we use a fixed list
+ else
+ {
+ m_status = Job_Finished;
+ m_reply.reset();
+ deferToFixedList();
+ return;
+ }
+}
+
+void ForgeMirrors::deferToFixedList()
+{
+ m_mirrors.clear();
+ m_mirrors.append(
+ {"Minecraft Forge", "http://files.minecraftforge.net/forge_logo.png",
+ "http://files.minecraftforge.net/", "http://files.minecraftforge.net/maven/"});
+ m_mirrors.append({"Creeper Host",
+ "http://files.minecraftforge.net/forge_logo.png",
+ "https://www.creeperhost.net/link.php?id=1",
+ "http://new.creeperrepo.net/forge/maven/"});
+ injectDownloads();
+ emit succeeded(m_index_within_job);
+}
+
+void ForgeMirrors::parseMirrorList()
+{
+ m_status = Job_Finished;
+ auto data = m_reply->readAll();
+ m_reply.reset();
+ auto dataLines = data.split('\n');
+ for(auto line: dataLines)
+ {
+ auto elements = line.split('!');
+ if (elements.size() == 4)
+ {
+ m_mirrors.append({elements[0],elements[1],elements[2],elements[3]});
+ }
+ }
+ if(!m_mirrors.size())
+ deferToFixedList();
+ injectDownloads();
+ emit succeeded(m_index_within_job);
+}
+
+void ForgeMirrors::injectDownloads()
+{
+ // shuffle the mirrors randomly
+ std::random_device rd;
+ std::mt19937 rng(rd());
+ std::shuffle(m_mirrors.begin(), m_mirrors.end(), rng);
+
+ // tell parent to download the libs
+ for(auto lib: m_libs)
+ {
+ lib->setMirrors(m_mirrors);
+ m_parent_job->addNetAction(lib);
+ }
+}
+
+void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void ForgeMirrors::downloadReadyRead()
+{
+}
diff --git a/logic/minecraft/forge/ForgeMirrors.h b/logic/minecraft/forge/ForgeMirrors.h
new file mode 100644
index 00000000..0312829b
--- /dev/null
+++ b/logic/minecraft/forge/ForgeMirrors.h
@@ -0,0 +1,61 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "ForgeXzDownload.h"
+
+#include "net/NetAction.h"
+#include "net/HttpMetaCache.h"
+#include "net/NetJob.h"
+
+#include <QFile>
+#include <QTemporaryFile>
+
+typedef std::shared_ptr<class ForgeMirrors> ForgeMirrorsPtr;
+
+class ForgeMirrors : public NetAction
+{
+ Q_OBJECT
+public:
+ QList<ForgeXzDownloadPtr> m_libs;
+ NetJobPtr m_parent_job;
+ QList<ForgeMirror> m_mirrors;
+
+public:
+ explicit ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
+ QString mirrorlist);
+ static ForgeMirrorsPtr make(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
+ QString mirrorlist)
+ {
+ return ForgeMirrorsPtr(new ForgeMirrors(libs, parent_job, mirrorlist));
+ }
+ virtual ~ForgeMirrors(){};
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+private:
+ void parseMirrorList();
+ void deferToFixedList();
+ void injectDownloads();
+
+public
+slots:
+ virtual void start();
+};
diff --git a/logic/minecraft/forge/ForgeVersion.cpp b/logic/minecraft/forge/ForgeVersion.cpp
new file mode 100644
index 00000000..b859a28c
--- /dev/null
+++ b/logic/minecraft/forge/ForgeVersion.cpp
@@ -0,0 +1,55 @@
+#include "ForgeVersion.h"
+#include "minecraft/VersionFilterData.h"
+#include <QObject>
+
+QString ForgeVersion::name()
+{
+ return "Forge " + jobbuildver;
+}
+
+QString ForgeVersion::descriptor()
+{
+ return universal_filename;
+}
+
+QString ForgeVersion::typeString() const
+{
+ if (is_recommended)
+ return QObject::tr("Recommended");
+ return QString();
+}
+
+bool ForgeVersion::operator<(BaseVersion &a)
+{
+ ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
+ if (!pa)
+ return true;
+ return m_buildnr < pa->m_buildnr;
+}
+
+bool ForgeVersion::operator>(BaseVersion &a)
+{
+ ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
+ if (!pa)
+ return false;
+ return m_buildnr > pa->m_buildnr;
+}
+
+bool ForgeVersion::usesInstaller()
+{
+ if(installer_url.isEmpty())
+ return false;
+ if(g_VersionFilterData.forgeInstallerBlacklist.contains(mcver))
+ return false;
+ return true;
+}
+
+QString ForgeVersion::filename()
+{
+ return usesInstaller() ? installer_filename : universal_filename;
+}
+
+QString ForgeVersion::url()
+{
+ return usesInstaller() ? installer_url : universal_url;
+}
diff --git a/logic/minecraft/forge/ForgeVersion.h b/logic/minecraft/forge/ForgeVersion.h
new file mode 100644
index 00000000..e77d32f1
--- /dev/null
+++ b/logic/minecraft/forge/ForgeVersion.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <QString>
+#include <memory>
+#include "BaseVersion.h"
+
+struct ForgeVersion;
+typedef std::shared_ptr<ForgeVersion> ForgeVersionPtr;
+
+struct ForgeVersion : public BaseVersion
+{
+ virtual QString descriptor() override;
+ virtual QString name() override;
+ virtual QString typeString() const override;
+ virtual bool operator<(BaseVersion &a) override;
+ virtual bool operator>(BaseVersion &a) override;
+
+ QString filename();
+ QString url();
+
+ enum
+ {
+ Invalid,
+ Legacy,
+ Gradle
+ } type = Invalid;
+
+ bool usesInstaller();
+
+ int m_buildnr = 0;
+ QString branch;
+ QString universal_url;
+ QString changelog_url;
+ QString installer_url;
+ QString jobbuildver;
+ QString mcver;
+ QString mcver_sane;
+ QString universal_filename;
+ QString installer_filename;
+ bool is_recommended = false;
+};
+
+Q_DECLARE_METATYPE(ForgeVersionPtr)
diff --git a/logic/minecraft/forge/ForgeVersionList.cpp b/logic/minecraft/forge/ForgeVersionList.cpp
new file mode 100644
index 00000000..9b418310
--- /dev/null
+++ b/logic/minecraft/forge/ForgeVersionList.cpp
@@ -0,0 +1,450 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ForgeVersionList.h"
+#include "ForgeVersion.h"
+
+#include "net/NetJob.h"
+#include "net/URLConstants.h"
+#include "Env.h"
+
+#include <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include <QDebug>
+
+ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *ForgeVersionList::getLoadTask()
+{
+ return new ForgeListLoadTask(this);
+}
+
+bool ForgeVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr ForgeVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int ForgeVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+int ForgeVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ForgeVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<ForgeVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ case VersionRole:
+ return version->name();
+
+ case VersionIdRole:
+ return version->descriptor();
+
+ case ParentGameVersionRole:
+ return version->mcver_sane;
+
+ case RecommendedRole:
+ return version->is_recommended;
+
+ case BranchRole:
+ return version->branch;
+
+ default:
+ return QVariant();
+ }
+}
+
+QList<BaseVersionList::ModelRoles> ForgeVersionList::providesRoles()
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole};
+}
+
+BaseVersionPtr ForgeVersionList::getLatestStable() const
+{
+ return BaseVersionPtr();
+}
+
+void ForgeVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ endResetModel();
+ // NOW SORT!!
+ // sort();
+}
+
+void ForgeVersionList::sortVersions()
+{
+ // NO-OP for now
+}
+
+ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task()
+{
+ m_list = vlist;
+}
+
+void ForgeListLoadTask::executeTask()
+{
+ setStatus(tr("Fetching Forge version lists..."));
+ auto job = new NetJob("Version index");
+ // we do not care if the version is stale or not.
+ auto forgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "list.json");
+ auto gradleForgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "json");
+
+ // verify by poking the server.
+ forgeListEntry->stale = true;
+ gradleForgeListEntry->stale = true;
+
+ job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL),
+ forgeListEntry));
+ job->addNetAction(gradleListDownload = CacheDownload::make(
+ QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry));
+
+ connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
+ connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed()));
+
+ listJob.reset(job);
+ connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
+ connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ listJob->start();
+}
+
+bool ForgeListLoadTask::abort()
+{
+ return listJob->abort();
+}
+
+bool ForgeListLoadTask::parseForgeList(QList<BaseVersionPtr> &out)
+{
+ QByteArray data;
+ {
+ auto dlJob = listDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ data = listFile.readAll();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing version list JSON:" + jsonError.errorString());
+ return false;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing version list JSON: JSON root is not an object");
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // Now, get the array of versions.
+ if (!root.value("builds").isArray())
+ {
+ emitFailed(
+ "Error parsing version list JSON: version list object is missing 'builds' array");
+ return false;
+ }
+ QJsonArray builds = root.value("builds").toArray();
+
+ for (int i = 0; i < builds.count(); i++)
+ {
+ // Load the version info.
+ if (!builds[i].isObject())
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+ QJsonObject obj = builds[i].toObject();
+ int build_nr = obj.value("build").toDouble(0);
+ if (!build_nr)
+ continue;
+ QJsonArray files = obj.value("files").toArray();
+ QString url, jobbuildver, mcver, buildtype, universal_filename;
+ QString changelog_url, installer_url;
+ QString installer_filename;
+ bool valid = false;
+ for (int j = 0; j < files.count(); j++)
+ {
+ if (!files[j].isObject())
+ {
+ continue;
+ }
+ QJsonObject file = files[j].toObject();
+ buildtype = file.value("buildtype").toString();
+ if ((buildtype == "client" || buildtype == "universal") && !valid)
+ {
+ mcver = file.value("mcver").toString();
+ url = file.value("url").toString();
+ jobbuildver = file.value("jobbuildver").toString();
+ int lastSlash = url.lastIndexOf('/');
+ universal_filename = url.mid(lastSlash + 1);
+ valid = true;
+ }
+ else if (buildtype == "changelog")
+ {
+ QString ext = file.value("ext").toString();
+ if (ext.isEmpty())
+ {
+ continue;
+ }
+ changelog_url = file.value("url").toString();
+ }
+ else if (buildtype == "installer")
+ {
+ installer_url = file.value("url").toString();
+ int lastSlash = installer_url.lastIndexOf('/');
+ installer_filename = installer_url.mid(lastSlash + 1);
+ }
+ }
+ if (valid)
+ {
+ // Now, we construct the version object and add it to the list.
+ std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
+ fVersion->universal_url = url;
+ fVersion->changelog_url = changelog_url;
+ fVersion->installer_url = installer_url;
+ fVersion->jobbuildver = jobbuildver;
+ fVersion->mcver = fVersion->mcver_sane = mcver;
+ fVersion->installer_filename = installer_filename;
+ fVersion->universal_filename = universal_filename;
+ fVersion->m_buildnr = build_nr;
+ fVersion->type = ForgeVersion::Legacy;
+ out.append(fVersion);
+ }
+ }
+
+ return true;
+}
+
+bool ForgeListLoadTask::parseForgeGradleList(QList<BaseVersionPtr> &out)
+{
+ QMap<int, std::shared_ptr<ForgeVersion>> lookup;
+ QByteArray data;
+ {
+ auto dlJob = gradleListDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ data = listFile.readAll();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString());
+ return false;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing gradle version list JSON: JSON root is not an object");
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // we probably could hard code these, but it might still be worth doing it this way
+ const QString webpath = root.value("webpath").toString();
+ const QString artifact = root.value("artifact").toString();
+
+ QJsonObject numbers = root.value("number").toObject();
+ for (auto it = numbers.begin(); it != numbers.end(); ++it)
+ {
+ QJsonObject number = it.value().toObject();
+ std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
+ fVersion->m_buildnr = number.value("build").toDouble();
+ if(fVersion->m_buildnr >= 953 && fVersion->m_buildnr <= 965)
+ {
+ qDebug() << fVersion->m_buildnr;
+ }
+ fVersion->jobbuildver = number.value("version").toString();
+ fVersion->branch = number.value("branch").toString("");
+ fVersion->mcver = number.value("mcversion").toString();
+ fVersion->universal_filename = "";
+ fVersion->installer_filename = "";
+ // HACK: here, we fix the minecraft version used by forge.
+ // HACK: this will inevitably break (later)
+ // FIXME: replace with a dictionary
+ fVersion->mcver_sane = fVersion->mcver;
+ fVersion->mcver_sane.replace("_pre", "-pre");
+
+ QString universal_filename, installer_filename;
+ QJsonArray files = number.value("files").toArray();
+ for (auto fIt = files.begin(); fIt != files.end(); ++fIt)
+ {
+ // TODO with gradle we also get checksums, use them
+ QJsonArray file = (*fIt).toArray();
+ if (file.size() < 3)
+ {
+ continue;
+ }
+
+ QString extension = file.at(0).toString();
+ QString part = file.at(1).toString();
+ QString checksum = file.at(2).toString();
+
+ // insane form of mcver is used here
+ QString longVersion = fVersion->mcver + "-" + fVersion->jobbuildver;
+ if (!fVersion->branch.isEmpty())
+ {
+ longVersion = longVersion + "-" + fVersion->branch;
+ }
+ QString filename = artifact + "-" + longVersion + "-" + part + "." + extension;
+
+ QString url = QString("%1/%2/%3")
+ .arg(webpath)
+ .arg(longVersion)
+ .arg(filename);
+
+ if (part == "installer")
+ {
+ fVersion->installer_url = url;
+ installer_filename = filename;
+ }
+ else if (part == "universal")
+ {
+ fVersion->universal_url = url;
+ universal_filename = filename;
+ }
+ else if (part == "changelog")
+ {
+ fVersion->changelog_url = url;
+ }
+ }
+ if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty())
+ {
+ continue;
+ }
+ fVersion->universal_filename = universal_filename;
+ fVersion->installer_filename = installer_filename;
+ fVersion->type = ForgeVersion::Gradle;
+ out.append(fVersion);
+ lookup[fVersion->m_buildnr] = fVersion;
+ }
+ QJsonObject promos = root.value("promos").toObject();
+ for (auto it = promos.begin(); it != promos.end(); ++it)
+ {
+ QString key = it.key();
+ int build = it.value().toInt();
+ QRegularExpression regexp("^(?<mcversion>[0-9]+(.[0-9]+)*)-(?<label>[a-z]+)$");
+ auto match = regexp.match(key);
+ if(!match.hasMatch())
+ {
+ qDebug() << key << "doesn't match." << "build" << build;
+ continue;
+ }
+
+ QString label = match.captured("label");
+ if(label != "recommended")
+ {
+ continue;
+ }
+ QString mcversion = match.captured("mcversion");
+ qDebug() << "Forge build" << build << "is the" << label << "for Minecraft" << mcversion << QString("<%1>").arg(key);
+ lookup[build]->is_recommended = true;
+ }
+ return true;
+}
+
+void ForgeListLoadTask::listDownloaded()
+{
+ QList<BaseVersionPtr> list;
+ bool ret = true;
+ if (!parseForgeList(list))
+ {
+ ret = false;
+ }
+ if (!parseForgeGradleList(list))
+ {
+ ret = false;
+ }
+
+ if (!ret)
+ {
+ return;
+ }
+ std::sort(list.begin(), list.end(), [](const BaseVersionPtr & l, const BaseVersionPtr & r)
+ { return (*l > *r); });
+
+ m_list->updateListData(list);
+
+ emitSucceeded();
+ return;
+}
+
+void ForgeListLoadTask::listFailed()
+{
+ auto &reply = listDownload->m_reply;
+ if (reply)
+ {
+ qCritical() << "Getting forge version list failed: " << reply->errorString();
+ }
+ else
+ {
+ qCritical() << "Getting forge version list failed for reasons unknown.";
+ }
+}
+
+void ForgeListLoadTask::gradleListFailed()
+{
+ auto &reply = gradleListDownload->m_reply;
+ if (reply)
+ {
+ qCritical() << "Getting forge version list failed: " << reply->errorString();
+ }
+ else
+ {
+ qCritical() << "Getting forge version list failed for reasons unknown.";
+ }
+}
diff --git a/logic/minecraft/forge/ForgeVersionList.h b/logic/minecraft/forge/ForgeVersionList.h
new file mode 100644
index 00000000..308503e3
--- /dev/null
+++ b/logic/minecraft/forge/ForgeVersionList.h
@@ -0,0 +1,90 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "ForgeVersion.h"
+
+#include <QObject>
+#include <QAbstractListModel>
+#include <QUrl>
+#include <QNetworkReply>
+
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT ForgeVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ friend class ForgeListLoadTask;
+
+ explicit ForgeVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask() override;
+ virtual bool isLoaded() override;
+ virtual const BaseVersionPtr at(int i) const override;
+ virtual int count() const override;
+ virtual void sortVersions() override;
+
+ virtual BaseVersionPtr getLatestStable() const override;
+
+ ForgeVersionPtr findVersionByVersionNr(QString version);
+
+ virtual QVariant data(const QModelIndex &index, int role) const override;
+ virtual QList<ModelRoles> providesRoles() override;
+
+ virtual int columnCount(const QModelIndex &parent) const override;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions) override;
+};
+
+class ForgeListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit ForgeListLoadTask(ForgeVersionList *vlist);
+
+ virtual void executeTask();
+ virtual bool abort();
+
+protected
+slots:
+ void listDownloaded();
+ void listFailed();
+ void gradleListFailed();
+
+protected:
+ NetJobPtr listJob;
+ ForgeVersionList *m_list;
+
+ CacheDownloadPtr listDownload;
+ CacheDownloadPtr gradleListDownload;
+
+private:
+ bool parseForgeList(QList<BaseVersionPtr> &out);
+ bool parseForgeGradleList(QList<BaseVersionPtr> &out);
+};
diff --git a/logic/minecraft/forge/ForgeXzDownload.cpp b/logic/minecraft/forge/ForgeXzDownload.cpp
new file mode 100644
index 00000000..6009e31e
--- /dev/null
+++ b/logic/minecraft/forge/ForgeXzDownload.cpp
@@ -0,0 +1,389 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Env.h"
+#include "ForgeXzDownload.h"
+#include <FileSystem.h>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include <QDir>
+#include <QDebug>
+
+ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction()
+{
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX");
+ m_status = Job_NotStarted;
+ m_url_path = relative_path;
+}
+
+void ForgeXzDownload::setMirrors(QList<ForgeMirror> &mirrors)
+{
+ m_mirror_index = 0;
+ m_mirrors = mirrors;
+ updateUrl();
+}
+
+void ForgeXzDownload::start()
+{
+ m_status = Job_InProgress;
+ if (!m_entry->stale)
+ {
+ m_status = Job_Finished;
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // can we actually create the real, final file?
+ if (!FS::ensureFilePathExists(m_target_path))
+ {
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ if (m_mirrors.empty())
+ {
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ qDebug() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1());
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ // TODO: log the reason why
+ m_status = Job_Failed;
+}
+
+void ForgeXzDownload::failAndTryNextMirror()
+{
+ m_status = Job_Failed;
+ int next = m_mirror_index + 1;
+ if(m_mirrors.size() == next)
+ m_mirror_index = 0;
+ else
+ m_mirror_index = next;
+
+ updateUrl();
+ emit failed(m_index_within_job);
+}
+
+void ForgeXzDownload::updateUrl()
+{
+ qDebug() << "Updating URL for " << m_url_path;
+ for (auto possible : m_mirrors)
+ {
+ qDebug() << "Possible: " << possible.name << " : " << possible.mirror_url;
+ }
+ QString aggregate = m_mirrors[m_mirror_index].mirror_url + m_url_path + ".pack.xz";
+ m_url = QUrl(aggregate);
+}
+
+void ForgeXzDownload::downloadFinished()
+{
+ //TEST: defer to other possible mirrors (autofail the first one)
+ /*
+ qDebug() <<"dl " << index_within_job << " mirror " << m_mirror_index;
+ if( m_mirror_index == 0)
+ {
+ qDebug() <<"dl " << index_within_job << " AUTOFAIL";
+ m_status = Job_Failed;
+ m_pack200_xz_file.close();
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ failAndTryNextMirror();
+ return;
+ }
+ */
+
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ if (m_pack200_xz_file.isOpen())
+ {
+ // we actually downloaded something! process and isntall it
+ decompressAndInstall();
+ return;
+ }
+ else
+ {
+ // something bad happened -- on the local machine!
+ m_status = Job_Failed;
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ // else the download failed
+ else
+ {
+ m_status = Job_Failed;
+ m_pack200_xz_file.close();
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ failAndTryNextMirror();
+ return;
+ }
+}
+
+void ForgeXzDownload::downloadReadyRead()
+{
+
+ if (!m_pack200_xz_file.isOpen())
+ {
+ if (!m_pack200_xz_file.open())
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ m_pack200_xz_file.write(m_reply->readAll());
+}
+
+#include "xz.h"
+#include "unpack200.h"
+#include <stdexcept>
+
+const size_t buffer_size = 8196;
+
+void ForgeXzDownload::decompressAndInstall()
+{
+ // rewind the downloaded temp file
+ m_pack200_xz_file.seek(0);
+ // de-xz'd file
+ QTemporaryFile pack200_file("./dl_temp.XXXXXX");
+ pack200_file.open();
+
+ bool xz_success = false;
+ // first, de-xz
+ {
+ uint8_t in[buffer_size];
+ uint8_t out[buffer_size];
+ struct xz_buf b;
+ struct xz_dec *s;
+ enum xz_ret ret;
+ xz_crc32_init();
+ xz_crc64_init();
+ s = xz_dec_init(XZ_DYNALLOC, 1 << 26);
+ if (s == nullptr)
+ {
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+ b.in = in;
+ b.in_pos = 0;
+ b.in_size = 0;
+ b.out = out;
+ b.out_pos = 0;
+ b.out_size = buffer_size;
+ while (!xz_success)
+ {
+ if (b.in_pos == b.in_size)
+ {
+ b.in_size = m_pack200_xz_file.read((char *)in, sizeof(in));
+ b.in_pos = 0;
+ }
+
+ ret = xz_dec_run(s, &b);
+
+ if (b.out_pos == sizeof(out))
+ {
+ if (pack200_file.write((char *)out, b.out_pos) != b.out_pos)
+ {
+ // msg = "Write error\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+
+ b.out_pos = 0;
+ }
+
+ if (ret == XZ_OK)
+ continue;
+
+ if (ret == XZ_UNSUPPORTED_CHECK)
+ {
+ // unsupported check. this is OK, but we should log this
+ continue;
+ }
+
+ if (pack200_file.write((char *)out, b.out_pos) != b.out_pos)
+ {
+ // write error
+ pack200_file.close();
+ xz_dec_end(s);
+ return;
+ }
+
+ switch (ret)
+ {
+ case XZ_STREAM_END:
+ xz_dec_end(s);
+ xz_success = true;
+ break;
+
+ case XZ_MEM_ERROR:
+ qCritical() << "Memory allocation failed\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_MEMLIMIT_ERROR:
+ qCritical() << "Memory usage limit reached\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_FORMAT_ERROR:
+ qCritical() << "Not a .xz file\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_OPTIONS_ERROR:
+ qCritical() << "Unsupported options in the .xz headers\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_DATA_ERROR:
+ case XZ_BUF_ERROR:
+ qCritical() << "File is corrupt\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ default:
+ qCritical() << "Bug!\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+ }
+ }
+ m_pack200_xz_file.remove();
+
+ // revert pack200
+ pack200_file.seek(0);
+ int handle_in = pack200_file.handle();
+ // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects.
+ if(handle_in == -1)
+ {
+ qCritical() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ FILE * file_in = fdopen(handle_in,"r");
+ if(!file_in)
+ {
+ qCritical() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ QFile qfile_out(m_target_path);
+ if(!qfile_out.open(QIODevice::WriteOnly))
+ {
+ qCritical() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ int handle_out = qfile_out.handle();
+ if(handle_out == -1)
+ {
+ qCritical() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ FILE * file_out = fdopen(handle_out,"w");
+ if(!file_out)
+ {
+ qCritical() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ try
+ {
+ unpack_200(file_in, file_out);
+ }
+ catch (std::runtime_error &err)
+ {
+ m_status = Job_Failed;
+ qCritical() << "Error unpacking " << pack200_file.fileName() << " : " << err.what();
+ QFile f(m_target_path);
+ if (f.exists())
+ f.remove();
+ failAndTryNextMirror();
+ return;
+ }
+ pack200_file.remove();
+
+ QFile jar_file(m_target_path);
+
+ if (!jar_file.open(QIODevice::ReadOnly))
+ {
+ jar_file.remove();
+ failAndTryNextMirror();
+ return;
+ }
+ m_entry->md5sum = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5)
+ .toHex()
+ .constData();
+ jar_file.close();
+
+ QFileInfo output_file_info(m_target_path);
+ m_entry->etag = m_reply->rawHeader("ETag").constData();
+ m_entry->local_changed_timestamp =
+ output_file_info.lastModified().toUTC().toMSecsSinceEpoch();
+ m_entry->stale = false;
+ ENV.metacache()->updateEntry(m_entry);
+
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+}
diff --git a/logic/minecraft/forge/ForgeXzDownload.h b/logic/minecraft/forge/ForgeXzDownload.h
new file mode 100644
index 00000000..45722812
--- /dev/null
+++ b/logic/minecraft/forge/ForgeXzDownload.h
@@ -0,0 +1,66 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "net/NetAction.h"
+#include "net/HttpMetaCache.h"
+#include <QFile>
+#include <QTemporaryFile>
+#include "ForgeMirror.h"
+
+typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr;
+
+class ForgeXzDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ MetaEntryPtr m_entry;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ QTemporaryFile m_pack200_xz_file;
+ /// mirror index (NOT OPTICS, I SWEAR)
+ int m_mirror_index = 0;
+ /// list of mirrors to use. Mirror has the url base
+ QList<ForgeMirror> m_mirrors;
+ /// path relative to the mirror base
+ QString m_url_path;
+
+public:
+ explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry);
+ static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry)
+ {
+ return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry));
+ }
+ virtual ~ForgeXzDownload(){};
+ void setMirrors(QList<ForgeMirror> & mirrors);
+
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public
+slots:
+ virtual void start();
+
+private:
+ void decompressAndInstall();
+ void failAndTryNextMirror();
+ void updateUrl();
+};
diff --git a/logic/minecraft/forge/LegacyForge.cpp b/logic/minecraft/forge/LegacyForge.cpp
new file mode 100644
index 00000000..aa2c8063
--- /dev/null
+++ b/logic/minecraft/forge/LegacyForge.cpp
@@ -0,0 +1,56 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/minecraft/forge/LegacyForge.h b/logic/minecraft/forge/LegacyForge.h
new file mode 100644
index 00000000..f51d5e85
--- /dev/null
+++ b/logic/minecraft/forge/LegacyForge.h
@@ -0,0 +1,25 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "minecraft/Mod.h"
+
+class MinecraftForge : public Mod
+{
+public:
+ MinecraftForge(const QString &file);
+ bool FixVersionIfNeeded(QString newVersion);
+};
diff --git a/logic/minecraft/ftb/FTBPlugin.cpp b/logic/minecraft/ftb/FTBPlugin.cpp
new file mode 100644
index 00000000..8a08f916
--- /dev/null
+++ b/logic/minecraft/ftb/FTBPlugin.cpp
@@ -0,0 +1,395 @@
+#include "FTBPlugin.h"
+#include "FTBVersion.h"
+#include "LegacyFTBInstance.h"
+#include "OneSixFTBInstance.h"
+#include <BaseInstance.h>
+#include <icons/IconList.h>
+#include <InstanceList.h>
+#include <minecraft/MinecraftVersionList.h>
+#include <settings/INISettingsObject.h>
+#include <FileSystem.h>
+#include "QDebug"
+#include <QXmlStreamReader>
+#include <QRegularExpression>
+
+struct FTBRecord
+{
+ QString dirName;
+ QString name;
+ QString logo;
+ QString iconKey;
+ QString mcVersion;
+ QString description;
+ QString instanceDir;
+ QString templateDir;
+ bool operator==(const FTBRecord other) const
+ {
+ return instanceDir == other.instanceDir;
+ }
+};
+
+inline uint qHash(FTBRecord record)
+{
+ return qHash(record.instanceDir);
+}
+
+QSet<FTBRecord> discoverFTBInstances(SettingsObjectPtr globalSettings)
+{
+ QSet<FTBRecord> records;
+ QDir dir = QDir(globalSettings->get("FTBLauncherLocal").toString());
+ QDir dataDir = QDir(globalSettings->get("FTBRoot").toString());
+ if (!dataDir.exists())
+ {
+ qDebug() << "The FTB directory specified does not exist. Please check your settings";
+ return records;
+ }
+ else if (!dir.exists())
+ {
+ qDebug() << "The FTB launcher data directory specified does not exist. Please check "
+ "your settings";
+ return records;
+ }
+ dir.cd("ModPacks");
+ auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name);
+ for (auto filename : allFiles)
+ {
+ if (!filename.endsWith(".xml"))
+ continue;
+ auto fpath = dir.absoluteFilePath(filename);
+ QFile f(fpath);
+ qDebug() << "Discovering FTB instances -- " << fpath;
+ if (!f.open(QFile::ReadOnly))
+ continue;
+
+ // read the FTB packs XML.
+ QXmlStreamReader reader(&f);
+ while (!reader.atEnd())
+ {
+ switch (reader.readNext())
+ {
+ case QXmlStreamReader::StartElement:
+ {
+ if (reader.name() == "modpack")
+ {
+ QXmlStreamAttributes attrs = reader.attributes();
+ FTBRecord record;
+ record.dirName = attrs.value("dir").toString();
+ record.instanceDir = dataDir.absoluteFilePath(record.dirName);
+ record.templateDir = dir.absoluteFilePath(record.dirName);
+ QDir test(record.instanceDir);
+ qDebug() << dataDir.absolutePath() << record.instanceDir << record.dirName;
+ if (!test.exists())
+ continue;
+ record.name = attrs.value("name").toString();
+ record.logo = attrs.value("logo").toString();
+ QString logo = record.logo;
+ record.iconKey = logo.remove(QRegularExpression("\\..*"));
+ auto customVersions = attrs.value("customMCVersions");
+ if (!customVersions.isNull())
+ {
+ QMap<QString, QString> versionMatcher;
+ QString customVersionsStr = customVersions.toString();
+ QStringList list = customVersionsStr.split(';');
+ for (auto item : list)
+ {
+ auto segment = item.split('^');
+ if (segment.size() != 2)
+ {
+ qCritical() << "FTB: Segment of size < 2 in "
+ << customVersionsStr;
+ continue;
+ }
+ versionMatcher[segment[0]] = segment[1];
+ }
+ auto actualVersion = attrs.value("version").toString();
+ if (versionMatcher.contains(actualVersion))
+ {
+ record.mcVersion = versionMatcher[actualVersion];
+ }
+ else
+ {
+ record.mcVersion = attrs.value("mcVersion").toString();
+ }
+ }
+ else
+ {
+ record.mcVersion = attrs.value("mcVersion").toString();
+ }
+ record.description = attrs.value("description").toString();
+ records.insert(record);
+ }
+ break;
+ }
+ case QXmlStreamReader::EndElement:
+ break;
+ case QXmlStreamReader::Characters:
+ break;
+ default:
+ break;
+ }
+ }
+ f.close();
+ }
+ return records;
+}
+
+InstancePtr loadInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record)
+{
+ InstancePtr inst;
+
+ auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg"));
+ m_settings->registerSetting("InstanceType", "Legacy");
+
+ qDebug() << "Loading existing " << record.name;
+
+ QString inst_type = m_settings->get("InstanceType").toString();
+ if (inst_type == "LegacyFTB")
+ {
+ inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ else if (inst_type == "OneSixFTB")
+ {
+ inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ else
+ {
+ return nullptr;
+ }
+ qDebug() << "Construction " << record.instanceDir;
+
+ SettingsObject::Lock lock(inst->settings());
+ inst->init();
+ qDebug() << "Init " << record.instanceDir;
+ inst->setGroupInitial("FTB");
+ /**
+ * FIXME: this does not respect the user's preferences. BUT, it would work nicely with the planned pack support
+ * -> instead of changing the user values, change pack values (defaults you can look at and revert to)
+ */
+ /*
+ inst->setName(record.name);
+ inst->setIconKey(record.iconKey);
+ inst->setNotes(record.description);
+ */
+ if (inst->intendedVersionId() != record.mcVersion)
+ {
+ inst->setIntendedVersionId(record.mcVersion);
+ }
+ qDebug() << "Post-Process " << record.instanceDir;
+ if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap))
+ {
+ return nullptr;
+ }
+ qDebug() << "Final " << record.instanceDir;
+ return inst;
+}
+
+InstancePtr createInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record)
+{
+ QDir rootDir(record.instanceDir);
+
+ InstancePtr inst;
+
+ qDebug() << "Converting " << record.name << " as new.";
+
+ auto mcVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", record.mcVersion));
+ if (!mcVersion)
+ {
+ qCritical() << "Can't load instance " << record.instanceDir
+ << " because minecraft version " << record.mcVersion
+ << " can't be resolved.";
+ return nullptr;
+ }
+
+ if (!rootDir.exists() && !rootDir.mkpath("."))
+ {
+ qCritical() << "Can't create instance folder" << record.instanceDir;
+ return nullptr;
+ }
+
+ auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg"));
+ m_settings->registerSetting("InstanceType", "Legacy");
+
+ if (mcVersion->usesLegacyLauncher())
+ {
+ m_settings->set("InstanceType", "LegacyFTB");
+ inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ else
+ {
+ m_settings->set("InstanceType", "OneSixFTB");
+ inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ // initialize
+ {
+ SettingsObject::Lock lock(inst->settings());
+ inst->setIntendedVersionId(mcVersion->descriptor());
+ inst->init();
+ inst->setGroupInitial("FTB");
+ inst->setName(record.name);
+ inst->setIconKey(record.iconKey);
+ inst->setNotes(record.description);
+ qDebug() << "Post-Process " << record.instanceDir;
+ if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap))
+ {
+ return nullptr;
+ }
+ }
+ return inst;
+}
+
+void FTBPlugin::loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList)
+{
+ // nothing to load when we don't have
+ if (globalSettings->get("TrackFTBInstances").toBool() != true)
+ {
+ return;
+ }
+
+ auto records = discoverFTBInstances(globalSettings);
+ if (!records.size())
+ {
+ qDebug() << "No FTB instances to load.";
+ return;
+ }
+ qDebug() << "Loading FTB instances! -- got " << records.size();
+ // process the records we acquired.
+ for (auto record : records)
+ {
+ qDebug() << "Loading FTB instance from " << record.instanceDir;
+ QString iconKey = record.iconKey;
+ ENV.icons()->addIcon(iconKey, iconKey, FS::PathCombine(record.templateDir, record.logo), MMCIcon::Transient);
+ auto settingsFilePath = FS::PathCombine(record.instanceDir, "instance.cfg");
+ qDebug() << "ICON get!";
+
+ if (QFileInfo(settingsFilePath).exists())
+ {
+ auto instPtr = loadInstance(globalSettings, groupMap, record);
+ if (!instPtr)
+ {
+ qWarning() << "Couldn't load instance config:" << settingsFilePath;
+ if(!QFile::remove(settingsFilePath))
+ {
+ qWarning() << "Couldn't remove broken instance config!";
+ continue;
+ }
+ // failed to load, but removed the poisonous file
+ }
+ else
+ {
+ tempList.append(InstancePtr(instPtr));
+ continue;
+ }
+ }
+ auto instPtr = createInstance(globalSettings, groupMap, record);
+ if (!instPtr)
+ {
+ qWarning() << "Couldn't create FTB instance!";
+ continue;
+ }
+ tempList.append(InstancePtr(instPtr));
+ }
+}
+
+#ifdef Q_OS_WIN32
+#include <windows.h>
+static const int APPDATA_BUFFER_SIZE = 1024;
+#endif
+
+static QString getLocalCacheStorageLocation()
+{
+ QString ftbDefault;
+#ifdef Q_OS_WIN32
+ wchar_t buf[APPDATA_BUFFER_SIZE];
+ if (GetEnvironmentVariableW(L"LOCALAPPDATA", buf, APPDATA_BUFFER_SIZE)) // local
+ {
+ ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
+ }
+ else if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE)) // roaming
+ {
+ ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
+ }
+ else
+ {
+ qCritical() << "Your LOCALAPPDATA and APPDATA folders are missing!"
+ " If you are on windows, this means your system is broken.";
+ }
+#elif defined(Q_OS_MAC)
+ ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher");
+#else
+ ftbDefault = QDir::home().absoluteFilePath(".ftblauncher");
+#endif
+ return ftbDefault;
+}
+
+
+static QString getRoamingStorageLocation()
+{
+ QString ftbDefault;
+#ifdef Q_OS_WIN32
+ wchar_t buf[APPDATA_BUFFER_SIZE];
+ QString cacheStorage;
+ if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE))
+ {
+ ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
+ }
+ else
+ {
+ qCritical() << "Your APPDATA folder is missing! If you are on windows, this means your system is broken.";
+ }
+#elif defined(Q_OS_MAC)
+ ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher");
+#else
+ ftbDefault = QDir::home().absoluteFilePath(".ftblauncher");
+#endif
+ return ftbDefault;
+}
+
+void FTBPlugin::initialize(SettingsObjectPtr globalSettings)
+{
+ // FTB
+ globalSettings->registerSetting("TrackFTBInstances", false);
+ QString ftbRoaming = getRoamingStorageLocation();
+ QString ftbLocal = getLocalCacheStorageLocation();
+
+ globalSettings->registerSetting("FTBLauncherRoaming", ftbRoaming);
+ globalSettings->registerSetting("FTBLauncherLocal", ftbLocal);
+ qDebug() << "FTB Launcher paths:" << globalSettings->get("FTBLauncherRoaming").toString()
+ << "and" << globalSettings->get("FTBLauncherLocal").toString();
+
+ globalSettings->registerSetting("FTBRoot");
+ if (globalSettings->get("FTBRoot").isNull())
+ {
+ QString ftbRoot;
+ QFile f(QDir(globalSettings->get("FTBLauncherRoaming").toString()).absoluteFilePath("ftblaunch.cfg"));
+ qDebug() << "Attempting to read" << f.fileName();
+ if (f.open(QFile::ReadOnly))
+ {
+ const QString data = QString::fromLatin1(f.readAll());
+ QRegularExpression exp("installPath=(.*)");
+ ftbRoot = QDir::cleanPath(exp.match(data).captured(1));
+#ifdef Q_OS_WIN32
+ if (!ftbRoot.isEmpty())
+ {
+ if (ftbRoot.at(0).isLetter() && ftbRoot.size() > 1 && ftbRoot.at(1) == '/')
+ {
+ ftbRoot.remove(1, 1);
+ }
+ }
+#endif
+ if (ftbRoot.isEmpty())
+ {
+ qDebug() << "Failed to get FTB root path";
+ }
+ else
+ {
+ qDebug() << "FTB is installed at" << ftbRoot;
+ globalSettings->set("FTBRoot", ftbRoot);
+ }
+ }
+ else
+ {
+ qWarning() << "Couldn't open" << f.fileName() << ":" << f.errorString();
+ qWarning() << "This is perfectly normal if you don't have FTB installed";
+ }
+ }
+}
diff --git a/logic/minecraft/ftb/FTBPlugin.h b/logic/minecraft/ftb/FTBPlugin.h
new file mode 100644
index 00000000..6851d8a5
--- /dev/null
+++ b/logic/minecraft/ftb/FTBPlugin.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <BaseInstance.h>
+
+#include "multimc_logic_export.h"
+
+// Pseudo-plugin for FTB related things. Super derpy!
+class MULTIMC_LOGIC_EXPORT FTBPlugin
+{
+public:
+ static void initialize(SettingsObjectPtr globalSettings);
+ static void loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList);
+};
diff --git a/logic/minecraft/ftb/FTBProfileStrategy.cpp b/logic/minecraft/ftb/FTBProfileStrategy.cpp
new file mode 100644
index 00000000..c4bfa27b
--- /dev/null
+++ b/logic/minecraft/ftb/FTBProfileStrategy.cpp
@@ -0,0 +1,128 @@
+#include "FTBProfileStrategy.h"
+#include "OneSixFTBInstance.h"
+
+#include "minecraft/VersionBuildError.h"
+#include "minecraft/MinecraftVersionList.h"
+#include <FileSystem.h>
+
+#include <QDir>
+#include <QUuid>
+#include <QJsonDocument>
+#include <QJsonArray>
+
+FTBProfileStrategy::FTBProfileStrategy(OneSixFTBInstance* instance) : OneSixProfileStrategy(instance)
+{
+}
+
+void FTBProfileStrategy::loadDefaultBuiltinPatches()
+{
+ // FIXME: this should be here, but it needs us to be able to deal with multiple libraries paths
+ // OneSixProfileStrategy::loadDefaultBuiltinPatches();
+ auto mcVersion = m_instance->intendedVersionId();
+
+ ProfilePatchPtr minecraftPatch;
+ {
+ auto mcJson = m_instance->versionsPath().absoluteFilePath(mcVersion + "/" + mcVersion + ".json");
+ // load up the base minecraft patch
+ if(QFile::exists(mcJson))
+ {
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
+ file->fileId = "net.minecraft";
+ file->name = QObject::tr("Minecraft (tracked)");
+ file->setVanilla(true);
+ if(file->version.isEmpty())
+ {
+ file->version = mcVersion;
+ }
+ minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
+ }
+ else
+ {
+ throw VersionIncomplete("net.minecraft");
+ }
+ minecraftPatch->setOrder(-2);
+ }
+ profile->appendPatch(minecraftPatch);
+
+ auto nativeInstance = dynamic_cast<OneSixFTBInstance *>(m_instance);
+ ProfilePatchPtr packPatch;
+ {
+ auto mcJson = m_instance->minecraftRoot() + "/pack.json";
+ // load up the base minecraft patch
+ if(QFile::exists(mcJson))
+ {
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
+
+ // adapt the loaded file - the FTB patch file format is different than ours.
+ file->addLibs = file->overwriteLibs;
+ file->overwriteLibs.clear();
+ file->shouldOverwriteLibs = false;
+ file->id.clear();
+ for(auto addLib: file->addLibs)
+ {
+ addLib->m_hint = "local";
+ addLib->setStoragePrefix(nativeInstance->librariesPath().absolutePath());
+ }
+ file->fileId = "org.multimc.ftb.pack";
+ file->setVanilla(true);
+ file->name = QObject::tr("%1 (FTB pack)").arg(m_instance->name());
+ if(file->version.isEmpty())
+ {
+ file->version = QObject::tr("Unknown");
+ QFile versionFile (FS::PathCombine(m_instance->instanceRoot(), "version"));
+ if(versionFile.exists())
+ {
+ if(versionFile.open(QIODevice::ReadOnly))
+ {
+ // FIXME: just guessing the encoding/charset here.
+ auto version = QString::fromUtf8(versionFile.readAll());
+ file->version = version;
+ }
+ }
+ }
+ packPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
+ }
+ else
+ {
+ throw VersionIncomplete("org.multimc.ftb.pack");
+ }
+ packPatch->setOrder(1);
+ }
+ profile->appendPatch(packPatch);
+
+}
+
+void FTBProfileStrategy::load()
+{
+ profile->clearPatches();
+
+ loadDefaultBuiltinPatches();
+ loadUserPatches();
+
+ profile->finalize();
+}
+
+bool FTBProfileStrategy::saveOrder(ProfileUtils::PatchOrder order)
+{
+ return false;
+}
+
+bool FTBProfileStrategy::resetOrder()
+{
+ return false;
+}
+
+bool FTBProfileStrategy::installJarMods(QStringList filepaths)
+{
+ return false;
+}
+
+bool FTBProfileStrategy::customizePatch(ProfilePatchPtr patch)
+{
+ return false;
+}
+
+bool FTBProfileStrategy::revertPatch(ProfilePatchPtr patch)
+{
+ return false;
+}
diff --git a/logic/minecraft/ftb/FTBProfileStrategy.h b/logic/minecraft/ftb/FTBProfileStrategy.h
new file mode 100644
index 00000000..522af098
--- /dev/null
+++ b/logic/minecraft/ftb/FTBProfileStrategy.h
@@ -0,0 +1,21 @@
+#pragma once
+#include "minecraft/ProfileStrategy.h"
+#include "minecraft/onesix/OneSixProfileStrategy.h"
+
+class OneSixFTBInstance;
+
+class FTBProfileStrategy : public OneSixProfileStrategy
+{
+public:
+ FTBProfileStrategy(OneSixFTBInstance * instance);
+ virtual ~FTBProfileStrategy() {};
+ virtual void load() override;
+ virtual bool resetOrder() override;
+ virtual bool saveOrder(ProfileUtils::PatchOrder order) override;
+ virtual bool installJarMods(QStringList filepaths) override;
+ virtual bool customizePatch (ProfilePatchPtr patch) override;
+ virtual bool revertPatch (ProfilePatchPtr patch) override;
+
+protected:
+ virtual void loadDefaultBuiltinPatches() override;
+};
diff --git a/logic/minecraft/ftb/FTBVersion.h b/logic/minecraft/ftb/FTBVersion.h
new file mode 100644
index 00000000..805319b4
--- /dev/null
+++ b/logic/minecraft/ftb/FTBVersion.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <minecraft/MinecraftVersion.h>
+
+class FTBVersion : public BaseVersion
+{
+public:
+ FTBVersion(MinecraftVersionPtr parent) : m_version(parent){};
+
+public:
+ virtual QString descriptor() override
+ {
+ return m_version->descriptor();
+ }
+
+ virtual QString name() override
+ {
+ return m_version->name();
+ }
+
+ virtual QString typeString() const override
+ {
+ return m_version->typeString();
+ }
+
+ MinecraftVersionPtr getMinecraftVersion()
+ {
+ return m_version;
+ }
+
+private:
+ MinecraftVersionPtr m_version;
+};
diff --git a/logic/minecraft/ftb/LegacyFTBInstance.cpp b/logic/minecraft/ftb/LegacyFTBInstance.cpp
new file mode 100644
index 00000000..a7091f1d
--- /dev/null
+++ b/logic/minecraft/ftb/LegacyFTBInstance.cpp
@@ -0,0 +1,27 @@
+#include "LegacyFTBInstance.h"
+#include <settings/INISettingsObject.h>
+#include <QDir>
+
+LegacyFTBInstance::LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) :
+ LegacyInstance(globalSettings, settings, rootDir)
+{
+}
+
+QString LegacyFTBInstance::id() const
+{
+ return "FTB/" + BaseInstance::id();
+}
+
+void LegacyFTBInstance::copy(const QDir &newDir)
+{
+ // set the target instance to be plain Legacy
+ INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg"));
+ settings_obj.registerSetting("InstanceType", "Legacy");
+ QString inst_type = settings_obj.get("InstanceType").toString();
+ settings_obj.set("InstanceType", "Legacy");
+}
+
+QString LegacyFTBInstance::typeName() const
+{
+ return tr("Legacy FTB");
+}
diff --git a/logic/minecraft/ftb/LegacyFTBInstance.h b/logic/minecraft/ftb/LegacyFTBInstance.h
new file mode 100644
index 00000000..3f3f5edf
--- /dev/null
+++ b/logic/minecraft/ftb/LegacyFTBInstance.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "minecraft/legacy/LegacyInstance.h"
+
+class LegacyFTBInstance : public LegacyInstance
+{
+ Q_OBJECT
+public:
+ explicit LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual QString id() const;
+ virtual void copy(const QDir &newDir);
+ virtual QString typeName() const;
+};
diff --git a/logic/minecraft/ftb/OneSixFTBInstance.cpp b/logic/minecraft/ftb/OneSixFTBInstance.cpp
new file mode 100644
index 00000000..19ec870a
--- /dev/null
+++ b/logic/minecraft/ftb/OneSixFTBInstance.cpp
@@ -0,0 +1,138 @@
+#include "OneSixFTBInstance.h"
+#include "FTBProfileStrategy.h"
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/GradleSpecifier.h"
+#include "tasks/SequentialTask.h"
+#include <settings/INISettingsObject.h>
+#include <FileSystem.h>
+
+#include <QJsonArray>
+
+OneSixFTBInstance::OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) :
+ OneSixInstance(globalSettings, settings, rootDir)
+{
+ m_globalSettings = globalSettings;
+}
+
+void OneSixFTBInstance::copy(const QDir &newDir)
+{
+ QStringList libraryNames;
+ // create patch file
+ {
+ qDebug()<< "Creating patch file for FTB instance...";
+ QFile f(minecraftRoot() + "/pack.json");
+ if (!f.open(QFile::ReadOnly))
+ {
+ qCritical() << "Couldn't open" << f.fileName() << ":" << f.errorString();
+ return;
+ }
+ QJsonObject root = QJsonDocument::fromJson(f.readAll()).object();
+ QJsonArray libs = root.value("libraries").toArray();
+ QJsonArray outLibs;
+ for (auto lib : libs)
+ {
+ QJsonObject libObj = lib.toObject();
+ libObj.insert("MMC-hint", QString("local"));
+ libObj.insert("insert", QString("prepend"));
+ libraryNames.append(libObj.value("name").toString());
+ outLibs.append(libObj);
+ }
+ root.remove("libraries");
+ root.remove("id");
+
+ // HACK HACK HACK HACK
+ // A workaround for a problem in MultiMC, triggered by a historical problem in FTB,
+ // triggered by Mojang getting their library versions wrong in 1.7.10
+ if(intendedVersionId() == "1.7.10")
+ {
+ auto insert = [&outLibs, &libraryNames](QString name)
+ {
+ QJsonObject libObj;
+ libObj.insert("insert", QString("replace"));
+ libObj.insert("name", name);
+ libraryNames.push_back(name);
+ outLibs.prepend(libObj);
+ };
+ insert("com.google.guava:guava:16.0");
+ insert("org.apache.commons:commons-lang3:3.2.1");
+ }
+ root.insert("+libraries", outLibs);
+ root.insert("order", 1);
+ root.insert("fileId", QString("org.multimc.ftb.pack.json"));
+ root.insert("name", name());
+ root.insert("mcVersion", intendedVersionId());
+ root.insert("version", intendedVersionId());
+ FS::ensureFilePathExists(newDir.absoluteFilePath("patches/ftb.json"));
+ QFile out(newDir.absoluteFilePath("patches/ftb.json"));
+ if (!out.open(QFile::WriteOnly | QFile::Truncate))
+ {
+ qCritical() << "Couldn't open" << out.fileName() << ":" << out.errorString();
+ return;
+ }
+ out.write(QJsonDocument(root).toJson());
+ }
+ // copy libraries
+ {
+ qDebug() << "Copying FTB libraries";
+ for (auto library : libraryNames)
+ {
+ GradleSpecifier lib(library);
+ const QString out = QDir::current().absoluteFilePath("libraries/" + lib.toPath());
+ if (QFile::exists(out))
+ {
+ continue;
+ }
+ if (!FS::ensureFilePathExists(out))
+ {
+ qCritical() << "Couldn't create folder structure for" << out;
+ }
+ if (!QFile::copy(librariesPath().absoluteFilePath(lib.toPath()), out))
+ {
+ qCritical() << "Couldn't copy" << QString(lib);
+ }
+ }
+ }
+ // now set the target instance to be plain OneSix
+ INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg"));
+ settings_obj.registerSetting("InstanceType", "Legacy");
+ QString inst_type = settings_obj.get("InstanceType").toString();
+ settings_obj.set("InstanceType", "OneSix");
+}
+
+QString OneSixFTBInstance::id() const
+{
+ return "FTB/" + BaseInstance::id();
+}
+
+QDir OneSixFTBInstance::librariesPath() const
+{
+ return QDir(m_globalSettings->get("FTBRoot").toString() + "/libraries");
+}
+
+QDir OneSixFTBInstance::versionsPath() const
+{
+ return QDir(m_globalSettings->get("FTBRoot").toString() + "/versions");
+}
+
+bool OneSixFTBInstance::providesVersionFile() const
+{
+ return true;
+}
+
+void OneSixFTBInstance::createProfile()
+{
+ m_version.reset(new MinecraftProfile(new FTBProfileStrategy(this)));
+}
+
+std::shared_ptr<Task> OneSixFTBInstance::createUpdateTask()
+{
+ return OneSixInstance::createUpdateTask();
+}
+
+QString OneSixFTBInstance::typeName() const
+{
+ return tr("OneSix FTB");
+}
+
+#include "OneSixFTBInstance.moc"
diff --git a/logic/minecraft/ftb/OneSixFTBInstance.h b/logic/minecraft/ftb/OneSixFTBInstance.h
new file mode 100644
index 00000000..09a2dcb0
--- /dev/null
+++ b/logic/minecraft/ftb/OneSixFTBInstance.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "minecraft/onesix/OneSixInstance.h"
+
+class OneSixFTBInstance : public OneSixInstance
+{
+ Q_OBJECT
+public:
+ explicit OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual ~OneSixFTBInstance(){};
+
+ void copy(const QDir &newDir) override;
+
+ virtual void createProfile() override;
+
+ virtual std::shared_ptr<Task> createUpdateTask() override;
+
+ virtual QString id() const override;
+
+ QDir librariesPath() const override;
+ QDir versionsPath() const override;
+ bool providesVersionFile() const override;
+ virtual QString typeName() const override;
+
+private:
+ SettingsObjectPtr m_globalSettings;
+};
diff --git a/logic/minecraft/LegacyInstance.cpp b/logic/minecraft/legacy/LegacyInstance.cpp
index 227fd601..056079f6 100644
--- a/logic/minecraft/LegacyInstance.cpp
+++ b/logic/minecraft/legacy/LegacyInstance.cpp
@@ -20,7 +20,7 @@
#include "LegacyInstance.h"
-#include "minecraft/LegacyUpdate.h"
+#include "minecraft/legacy/LegacyUpdate.h"
#include "icons/IconList.h"
#include "launch/LaunchTask.h"
#include <launch/steps/LaunchMinecraft.h>
diff --git a/logic/minecraft/LegacyInstance.h b/logic/minecraft/legacy/LegacyInstance.h
index a2ab86de..a2ab86de 100644
--- a/logic/minecraft/LegacyInstance.h
+++ b/logic/minecraft/legacy/LegacyInstance.h
diff --git a/logic/minecraft/LegacyUpdate.cpp b/logic/minecraft/legacy/LegacyUpdate.cpp
index 2fd8e3cb..af787f8c 100644
--- a/logic/minecraft/LegacyUpdate.cpp
+++ b/logic/minecraft/legacy/LegacyUpdate.cpp
@@ -25,10 +25,10 @@
#include "LegacyUpdate.h"
-#include "minecraft/LwjglVersionList.h"
+#include "LwjglVersionList.h"
#include "minecraft/MinecraftVersionList.h"
#include "minecraft/ModList.h"
-#include "minecraft/LegacyInstance.h"
+#include "LegacyInstance.h"
#include <FileSystem.h>
LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
diff --git a/logic/minecraft/LegacyUpdate.h b/logic/minecraft/legacy/LegacyUpdate.h
index c52bf934..c52bf934 100644
--- a/logic/minecraft/LegacyUpdate.h
+++ b/logic/minecraft/legacy/LegacyUpdate.h
diff --git a/logic/minecraft/LwjglVersionList.cpp b/logic/minecraft/legacy/LwjglVersionList.cpp
index bb017368..bb017368 100644
--- a/logic/minecraft/LwjglVersionList.cpp
+++ b/logic/minecraft/legacy/LwjglVersionList.cpp
diff --git a/logic/minecraft/LwjglVersionList.h b/logic/minecraft/legacy/LwjglVersionList.h
index f043f6e2..f043f6e2 100644
--- a/logic/minecraft/LwjglVersionList.h
+++ b/logic/minecraft/legacy/LwjglVersionList.h
diff --git a/logic/minecraft/liteloader/LiteLoaderInstaller.cpp b/logic/minecraft/liteloader/LiteLoaderInstaller.cpp
new file mode 100644
index 00000000..3fefe273
--- /dev/null
+++ b/logic/minecraft/liteloader/LiteLoaderInstaller.cpp
@@ -0,0 +1,141 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LiteLoaderInstaller.h"
+
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/RawLibrary.h"
+#include "minecraft/onesix/OneSixInstance.h"
+#include "minecraft/liteloader/LiteLoaderVersionList.h"
+#include "Exception.h"
+
+LiteLoaderInstaller::LiteLoaderInstaller() : BaseInstaller()
+{
+}
+
+void LiteLoaderInstaller::prepare(LiteLoaderVersionPtr version)
+{
+ m_version = version;
+}
+bool LiteLoaderInstaller::add(OneSixInstance *to)
+{
+ if (!BaseInstaller::add(to))
+ {
+ return false;
+ }
+
+ QJsonObject obj;
+
+ obj.insert("mainClass", QString("net.minecraft.launchwrapper.Launch"));
+ obj.insert("+tweakers", QJsonArray::fromStringList(QStringList() << m_version->tweakClass));
+ obj.insert("order", 10);
+
+ QJsonArray libraries;
+
+ for (auto rawLibrary : m_version->libraries)
+ {
+ libraries.append(rawLibrary->toJson());
+ }
+
+ // liteloader
+ {
+ RawLibrary liteloaderLib("com.mumfrey:liteloader:" + m_version->version);
+ liteloaderLib.setAbsoluteUrl(QString("http://dl.liteloader.com/versions/com/mumfrey/liteloader/%1/%2").arg(m_version->mcVersion, m_version->file));
+ QJsonObject llLibObj = liteloaderLib.toJson();
+ libraries.append(llLibObj);
+ }
+
+ obj.insert("+libraries", libraries);
+ obj.insert("name", QString("LiteLoader"));
+ obj.insert("fileId", id());
+ obj.insert("version", m_version->version);
+ obj.insert("mcVersion", to->intendedVersionId());
+
+ QFile file(filename(to->instanceRoot()));
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(QJsonDocument(obj).toJson());
+ file.close();
+
+ return true;
+}
+
+class LiteLoaderInstallTask : public Task
+{
+ Q_OBJECT
+public:
+ LiteLoaderInstallTask(LiteLoaderInstaller *installer, OneSixInstance *instance,
+ BaseVersionPtr version, QObject *parent)
+ : Task(parent), m_installer(installer), m_instance(instance), m_version(version)
+ {
+ }
+
+protected:
+ void executeTask() override
+ {
+ LiteLoaderVersionPtr liteloaderVersion =
+ std::dynamic_pointer_cast<LiteLoaderVersion>(m_version);
+ if (!liteloaderVersion)
+ {
+ return;
+ }
+ m_installer->prepare(liteloaderVersion);
+ if (!m_installer->add(m_instance))
+ {
+ emitFailed(tr("For reasons unknown, the LiteLoader installation failed. Check your "
+ "MultiMC log files for details."));
+ }
+ else
+ {
+ try
+ {
+ m_instance->reloadProfile();
+ emitSucceeded();
+ }
+ catch (Exception &e)
+ {
+ emitFailed(e.cause());
+ }
+ catch (...)
+ {
+ emitFailed(
+ tr("Failed to load the version description file for reasons unknown."));
+ }
+ }
+ }
+
+private:
+ LiteLoaderInstaller *m_installer;
+ OneSixInstance *m_instance;
+ BaseVersionPtr m_version;
+};
+
+Task *LiteLoaderInstaller::createInstallTask(OneSixInstance *instance,
+ BaseVersionPtr version,
+ QObject *parent)
+{
+ return new LiteLoaderInstallTask(this, instance, version, parent);
+}
+
+#include "LiteLoaderInstaller.moc"
diff --git a/logic/minecraft/liteloader/LiteLoaderInstaller.h b/logic/minecraft/liteloader/LiteLoaderInstaller.h
new file mode 100644
index 00000000..fe0aee3d
--- /dev/null
+++ b/logic/minecraft/liteloader/LiteLoaderInstaller.h
@@ -0,0 +1,39 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QMap>
+
+#include "BaseInstaller.h"
+#include "LiteLoaderVersionList.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT LiteLoaderInstaller : public BaseInstaller
+{
+public:
+ LiteLoaderInstaller();
+
+ void prepare(LiteLoaderVersionPtr version);
+ bool add(OneSixInstance *to) override;
+ virtual QString id() const override { return "com.mumfrey.liteloader"; }
+
+ Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override;
+
+private:
+ LiteLoaderVersionPtr m_version;
+};
diff --git a/logic/minecraft/liteloader/LiteLoaderVersionList.cpp b/logic/minecraft/liteloader/LiteLoaderVersionList.cpp
new file mode 100644
index 00000000..1d200e48
--- /dev/null
+++ b/logic/minecraft/liteloader/LiteLoaderVersionList.cpp
@@ -0,0 +1,275 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LiteLoaderVersionList.h"
+#include "Env.h"
+#include "net/URLConstants.h"
+#include "Exception.h"
+
+#include <QtXml>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QJsonParseError>
+
+#include <QtAlgorithms>
+
+#include <QtNetwork>
+
+LiteLoaderVersionList::LiteLoaderVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *LiteLoaderVersionList::getLoadTask()
+{
+ return new LLListLoadTask(this);
+}
+
+bool LiteLoaderVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr LiteLoaderVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int LiteLoaderVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second)
+{
+ auto left = std::dynamic_pointer_cast<LiteLoaderVersion>(first);
+ auto right = std::dynamic_pointer_cast<LiteLoaderVersion>(second);
+ return left->timestamp > right->timestamp;
+}
+
+void LiteLoaderVersionList::sortVersions()
+{
+ beginResetModel();
+ std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+ endResetModel();
+}
+
+QVariant LiteLoaderVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ case VersionRole:
+ return version->name();
+
+ case VersionIdRole:
+ return version->descriptor();
+
+ case ParentGameVersionRole:
+ return version->mcVersion;
+
+ case RecommendedRole:
+ return version->isLatest;
+
+ default:
+ return QVariant();
+ }
+}
+
+QList<BaseVersionList::ModelRoles> LiteLoaderVersionList::providesRoles()
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole};
+}
+
+BaseVersionPtr LiteLoaderVersionList::getLatestStable() const
+{
+ for (int i = 0; i < m_vlist.length(); i++)
+ {
+ auto ver = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist.at(i));
+ if (ver->isLatest)
+ {
+ return m_vlist.at(i);
+ }
+ }
+ return BaseVersionPtr();
+}
+
+void LiteLoaderVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+ endResetModel();
+}
+
+LLListLoadTask::LLListLoadTask(LiteLoaderVersionList *vlist)
+{
+ m_list = vlist;
+}
+
+LLListLoadTask::~LLListLoadTask()
+{
+}
+
+void LLListLoadTask::executeTask()
+{
+ setStatus(tr("Loading LiteLoader version list..."));
+ auto job = new NetJob("Version index");
+ // we do not care if the version is stale or not.
+ auto liteloaderEntry = ENV.metacache()->resolveEntry("liteloader", "versions.json");
+
+ // verify by poking the server.
+ liteloaderEntry->stale = true;
+
+ job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::LITELOADER_URL),
+ liteloaderEntry));
+
+ connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
+
+ listJob.reset(job);
+ connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
+ connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ listJob->start();
+}
+
+void LLListLoadTask::listFailed()
+{
+ emitFailed("Failed to load LiteLoader version list.");
+ return;
+}
+
+void LLListLoadTask::listDownloaded()
+{
+ QByteArray data;
+ {
+ auto dlJob = listDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ emitFailed("Failed to open the LiteLoader version list.");
+ return;
+ }
+ data = listFile.readAll();
+ listFile.close();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ 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;
+ }
+
+ const QJsonObject root = jsonDoc.object();
+
+ // Now, get the array of versions.
+ if (!root.value("versions").isObject())
+ {
+ emitFailed("Error parsing version list JSON: missing 'versions' object");
+ return;
+ }
+
+ auto meta = root.value("meta").toObject();
+ QString description = meta.value("description").toString(tr("This is a lightweight loader for mods that don't change game mechanics."));
+ QString defaultUrl = meta.value("url").toString("http://dl.liteloader.com");
+ QString authors = meta.value("authors").toString("Mumfrey");
+ auto versions = root.value("versions").toObject();
+
+ QList<BaseVersionPtr> tempList;
+ for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
+ {
+ const QString mcVersion = vIt.key();
+ QString latest;
+ const QJsonObject artefacts = vIt.value()
+ .toObject()
+ .value("artefacts")
+ .toObject()
+ .value("com.mumfrey:liteloader")
+ .toObject();
+ QList<BaseVersionPtr> perMcVersionList;
+ for (auto aIt = artefacts.begin(); aIt != artefacts.end(); ++aIt)
+ {
+ const QString identifier = aIt.key();
+ const QJsonObject artefact = aIt.value().toObject();
+ if (identifier == "latest")
+ {
+ latest = artefact.value("version").toString();
+ continue;
+ }
+ LiteLoaderVersionPtr version(new LiteLoaderVersion());
+ version->version = artefact.value("version").toString();
+ version->file = artefact.value("file").toString();
+ version->mcVersion = mcVersion;
+ version->md5 = artefact.value("md5").toString();
+ version->timestamp = artefact.value("timestamp").toString().toInt();
+ version->tweakClass = artefact.value("tweakClass").toString();
+ version->authors = authors;
+ version->description = description;
+ version->defaultUrl = defaultUrl;
+ const QJsonArray libs = artefact.value("libraries").toArray();
+ for (auto lIt = libs.begin(); lIt != libs.end(); ++lIt)
+ {
+ auto libobject = (*lIt).toObject();
+ try
+ {
+ auto lib = RawLibrary::fromJson(libobject, "versions.json");
+ // hack to make liteloader 1.7.10_00 work
+ if(lib->rawName() == GradleSpecifier("org.ow2.asm:asm-all:5.0.3"))
+ {
+ lib->setBaseUrl("http://repo.maven.apache.org/maven2/");
+ }
+ version->libraries.append(lib);
+ }
+ catch (Exception &e)
+ {
+ qCritical() << "Couldn't read JSON object:";
+ continue;
+ }
+ }
+ perMcVersionList.append(version);
+ }
+ for (auto version : perMcVersionList)
+ {
+ auto v = std::dynamic_pointer_cast<LiteLoaderVersion>(version);
+ v->isLatest = v->version == latest;
+ }
+ tempList.append(perMcVersionList);
+ }
+ m_list->updateListData(tempList);
+
+ emitSucceeded();
+}
diff --git a/logic/minecraft/liteloader/LiteLoaderVersionList.h b/logic/minecraft/liteloader/LiteLoaderVersionList.h
new file mode 100644
index 00000000..43bb270d
--- /dev/null
+++ b/logic/minecraft/liteloader/LiteLoaderVersionList.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+
+#include <QString>
+#include <QStringList>
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include <minecraft/RawLibrary.h>
+
+#include "multimc_logic_export.h"
+
+class LLListLoadTask;
+class QNetworkReply;
+
+class LiteLoaderVersion : public BaseVersion
+{
+public:
+ QString descriptor() override
+ {
+ if (isLatest)
+ {
+ return QObject::tr("Latest");
+ }
+ return QString();
+ }
+ QString typeString() const override
+ {
+ return mcVersion;
+ }
+ QString name() override
+ {
+ return version;
+ }
+
+ // important info
+ QString version;
+ QString file;
+ QString mcVersion;
+ QString md5;
+ int timestamp;
+ bool isLatest;
+ QString tweakClass;
+ QList<RawLibraryPtr> libraries;
+
+ // meta
+ QString defaultUrl;
+ QString description;
+ QString authors;
+};
+typedef std::shared_ptr<LiteLoaderVersion> LiteLoaderVersionPtr;
+
+class MULTIMC_LOGIC_EXPORT LiteLoaderVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ friend class LLListLoadTask;
+
+ explicit LiteLoaderVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask();
+ virtual bool isLoaded();
+ virtual const BaseVersionPtr at(int i) const;
+ virtual int count() const;
+ virtual void sortVersions();
+ virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const;
+ virtual QList< ModelRoles > providesRoles();
+
+ virtual BaseVersionPtr getLatestStable() const;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions);
+};
+
+class LLListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit LLListLoadTask(LiteLoaderVersionList *vlist);
+ ~LLListLoadTask();
+
+ virtual void executeTask();
+
+protected
+slots:
+ void listDownloaded();
+ void listFailed();
+
+protected:
+ NetJobPtr listJob;
+ CacheDownloadPtr listDownload;
+ LiteLoaderVersionList *m_list;
+};
+
+Q_DECLARE_METATYPE(LiteLoaderVersionPtr)
diff --git a/logic/minecraft/OneSixInstance.cpp b/logic/minecraft/onesix/OneSixInstance.cpp
index 9a3920cc..54381a99 100644
--- a/logic/minecraft/OneSixInstance.cpp
+++ b/logic/minecraft/onesix/OneSixInstance.cpp
@@ -16,20 +16,20 @@
#include <QIcon>
#include <QDebug>
-#include "minecraft/OneSixInstance.h"
+#include "OneSixInstance.h"
+#include "OneSixUpdate.h"
+#include "OneSixProfileStrategy.h"
-#include "minecraft/OneSixUpdate.h"
#include "minecraft/MinecraftProfile.h"
#include "minecraft/VersionBuildError.h"
#include "launch/LaunchTask.h"
-#include <launch/steps/PreLaunchCommand.h>
-#include <launch/steps/Update.h>
-#include <launch/steps/LaunchMinecraft.h>
-#include <launch/steps/PostLaunchCommand.h>
-#include <launch/steps/TextPrint.h>
-#include <launch/steps/ModMinecraftJar.h>
-#include <launch/steps/CheckJava.h>
-#include "minecraft/OneSixProfileStrategy.h"
+#include "launch/steps/PreLaunchCommand.h"
+#include "launch/steps/Update.h"
+#include "launch/steps/LaunchMinecraft.h"
+#include "launch/steps/PostLaunchCommand.h"
+#include "launch/steps/TextPrint.h"
+#include "launch/steps/ModMinecraftJar.h"
+#include "launch/steps/CheckJava.h"
#include "MMCZip.h"
#include "minecraft/AssetsUtils.h"
diff --git a/logic/minecraft/OneSixInstance.h b/logic/minecraft/onesix/OneSixInstance.h
index 824e3786..824e3786 100644
--- a/logic/minecraft/OneSixInstance.h
+++ b/logic/minecraft/onesix/OneSixInstance.h
diff --git a/logic/minecraft/OneSixProfileStrategy.cpp b/logic/minecraft/onesix/OneSixProfileStrategy.cpp
index 3e3534a0..35ca769f 100644
--- a/logic/minecraft/OneSixProfileStrategy.cpp
+++ b/logic/minecraft/onesix/OneSixProfileStrategy.cpp
@@ -1,6 +1,7 @@
-#include "minecraft/OneSixProfileStrategy.h"
+#include "OneSixProfileStrategy.h"
+#include "OneSixInstance.h"
+
#include "minecraft/VersionBuildError.h"
-#include "minecraft/OneSixInstance.h"
#include "minecraft/MinecraftVersionList.h"
#include "Env.h"
#include <FileSystem.h>
diff --git a/logic/minecraft/OneSixProfileStrategy.h b/logic/minecraft/onesix/OneSixProfileStrategy.h
index a6abe964..96c1ba7b 100644
--- a/logic/minecraft/OneSixProfileStrategy.h
+++ b/logic/minecraft/onesix/OneSixProfileStrategy.h
@@ -1,5 +1,5 @@
#pragma once
-#include "ProfileStrategy.h"
+#include "minecraft/ProfileStrategy.h"
class OneSixInstance;
diff --git a/logic/minecraft/OneSixUpdate.cpp b/logic/minecraft/onesix/OneSixUpdate.cpp
index 5babb0b6..3fd66920 100644
--- a/logic/minecraft/OneSixUpdate.cpp
+++ b/logic/minecraft/onesix/OneSixUpdate.cpp
@@ -15,6 +15,7 @@
#include "Env.h"
#include "OneSixUpdate.h"
+#include "OneSixInstance.h"
#include <QtNetwork>
@@ -28,8 +29,7 @@
#include "minecraft/MinecraftVersionList.h"
#include "minecraft/MinecraftProfile.h"
#include "minecraft/RawLibrary.h"
-#include "minecraft/OneSixInstance.h"
-#include "forge/ForgeMirrors.h"
+#include "minecraft/forge/ForgeMirrors.h"
#include "net/URLConstants.h"
#include "minecraft/AssetsUtils.h"
#include "Exception.h"
diff --git a/logic/minecraft/OneSixUpdate.h b/logic/minecraft/onesix/OneSixUpdate.h
index 6901e3d6..6901e3d6 100644
--- a/logic/minecraft/OneSixUpdate.h
+++ b/logic/minecraft/onesix/OneSixUpdate.h