summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--CMakeLists.txt13
-rw-r--r--MultiMC.cpp7
-rw-r--r--MultiMC.h7
-rw-r--r--gui/MainWindow.cpp161
-rw-r--r--gui/MainWindow.h26
-rw-r--r--gui/MainWindow.ui9
-rw-r--r--gui/dialogs/AccountListDialog.cpp112
-rw-r--r--gui/dialogs/AccountListDialog.h63
-rw-r--r--gui/dialogs/AccountListDialog.ui93
-rw-r--r--logic/BaseInstance.h6
-rw-r--r--logic/InstanceLauncher.cpp3
-rw-r--r--logic/LegacyInstance.cpp6
-rw-r--r--logic/LegacyInstance.h4
-rw-r--r--logic/OneSixInstance.cpp16
-rw-r--r--logic/OneSixInstance.h7
-rw-r--r--logic/auth/AuthenticateTask.cpp180
-rw-r--r--logic/auth/AuthenticateTask.h46
-rw-r--r--logic/auth/MojangAccount.cpp220
-rw-r--r--logic/auth/MojangAccount.h154
-rw-r--r--logic/auth/YggdrasilTask.cpp182
-rw-r--r--logic/auth/YggdrasilTask.h128
-rw-r--r--logic/lists/MojangAccountList.cpp349
-rw-r--r--logic/lists/MojangAccountList.h175
24 files changed, 1846 insertions, 129 deletions
diff --git a/.gitignore b/.gitignore
index 0af63a46..597bbbec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,11 @@ build
resources/CMakeFiles
resources/MultiMCLauncher.jar
*~
+*.swp
+
+# Ctags File
+tags
+
+# YouCompleteMe config stuff.
+.ycm_extra_conf.*
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a9de11d1..1bb3126b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -217,6 +217,8 @@ gui/dialogs/EditNotesDialog.h
gui/dialogs/EditNotesDialog.cpp
gui/dialogs/CustomMessageBox.h
gui/dialogs/CustomMessageBox.cpp
+gui/dialogs/AccountListDialog.h
+gui/dialogs/AccountListDialog.cpp
# GUI - widgets
gui/widgets/InstanceDelegate.h
@@ -269,6 +271,14 @@ logic/net/LoginTask.cpp
logic/net/S3ListBucket.h
logic/net/S3ListBucket.cpp
+# Yggdrasil login stuff
+logic/auth/MojangAccount.h
+logic/auth/MojangAccount.cpp
+logic/auth/YggdrasilTask.h
+logic/auth/YggdrasilTask.cpp
+logic/auth/AuthenticateTask.h
+logic/auth/AuthenticateTask.cpp
+
# legacy instances
logic/LegacyInstance.h
@@ -317,6 +327,8 @@ logic/lists/ForgeVersionList.h
logic/lists/ForgeVersionList.cpp
logic/lists/JavaVersionList.h
logic/lists/JavaVersionList.cpp
+logic/lists/MojangAccountList.h
+logic/lists/MojangAccountList.cpp
# misc model/view
logic/EnabledItemFilter.h
@@ -359,6 +371,7 @@ gui/dialogs/IconPickerDialog.ui
gui/dialogs/LegacyModEditDialog.ui
gui/dialogs/OneSixModEditDialog.ui
gui/dialogs/EditNotesDialog.ui
+gui/dialogs/AccountListDialog.ui
# Widgets/other
gui/widgets/MCModInfoFrame.ui
diff --git a/MultiMC.cpp b/MultiMC.cpp
index 0aaf31bf..17981755 100644
--- a/MultiMC.cpp
+++ b/MultiMC.cpp
@@ -10,6 +10,7 @@
#include "gui/MainWindow.h"
#include "gui/dialogs/VersionSelectDialog.h"
#include "logic/lists/InstanceList.h"
+#include "logic/lists/MojangAccountList.h"
#include "logic/lists/IconList.h"
#include "logic/lists/LwjglVersionList.h"
#include "logic/lists/MinecraftVersionList.h"
@@ -146,6 +147,12 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
connect(InstDirSetting, SIGNAL(settingChanged(const Setting &, QVariant)),
m_instances.get(), SLOT(on_InstFolderChanged(const Setting &, QVariant)));
+ // and accounts
+ m_accounts.reset(new MojangAccountList(this));
+ QLOG_INFO() << "Loading accounts...";
+ m_accounts->setListFilePath("accounts.json", true);
+ m_accounts->loadList();
+
// init the http meta cache
initHttpMetaCache();
diff --git a/MultiMC.h b/MultiMC.h
index cd4a5f7d..dba923b1 100644
--- a/MultiMC.h
+++ b/MultiMC.h
@@ -13,6 +13,7 @@ class LWJGLVersionList;
class HttpMetaCache;
class SettingsObject;
class InstanceList;
+class MojangAccountList;
class IconList;
class QNetworkAccessManager;
class ForgeVersionList;
@@ -57,6 +58,11 @@ public:
return m_instances;
}
+ std::shared_ptr<MojangAccountList> accounts()
+ {
+ return m_accounts;
+ }
+
std::shared_ptr<IconList> icons();
Status status()
@@ -101,6 +107,7 @@ private:
std::shared_ptr<QTranslator> m_mmc_translator;
std::shared_ptr<SettingsObject> m_settings;
std::shared_ptr<InstanceList> m_instances;
+ std::shared_ptr<MojangAccountList> m_accounts;
std::shared_ptr<IconList> m_icons;
std::shared_ptr<QNetworkAccessManager> m_qnam;
std::shared_ptr<HttpMetaCache> m_metacache;
diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp
index b8766d9a..6ae41f50 100644
--- a/gui/MainWindow.cpp
+++ b/gui/MainWindow.cpp
@@ -57,6 +57,7 @@
#include "gui/dialogs/IconPickerDialog.h"
#include "gui/dialogs/EditNotesDialog.h"
#include "gui/dialogs/CopyInstanceDialog.h"
+#include "gui/dialogs/AccountListDialog.h"
#include "gui/ConsoleWindow.h"
@@ -84,11 +85,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->setupUi(this);
setWindowTitle(QString("MultiMC %1").arg(MMC->version().toString()));
- // Set the selected instance to null
- m_selectedInstance = nullptr;
- // Set active instance to null.
- m_activeInst = nullptr;
-
// OSX magic.
setUnifiedTitleAndToolBarOnMac(true);
@@ -428,6 +424,12 @@ void MainWindow::on_actionSettings_triggered()
proxymodel->sort(0);
}
+void MainWindow::on_actionManageAccounts_triggered()
+{
+ AccountListDialog dialog(this);
+ dialog.exec();
+}
+
void MainWindow::on_actionReportBug_triggered()
{
openWebPage(QUrl("http://multimc.myjetbrains.com/youtrack/dashboard#newissue=yes"));
@@ -538,11 +540,7 @@ void MainWindow::instanceActivated(QModelIndex index)
NagUtils::checkJVMArgs(inst->settings().get("JvmArgs").toString(), this);
- bool autoLogin = inst->settings().get("AutoLogin").toBool();
- if (autoLogin)
- doAutoLogin();
- else
- doLogin();
+ doLogin();
}
void MainWindow::on_actionLaunchInstance_triggered()
@@ -554,106 +552,74 @@ void MainWindow::on_actionLaunchInstance_triggered()
}
}
-void MainWindow::doAutoLogin()
+void MainWindow::doLogin(const QString &errorMsg)
{
if (!m_selectedInstance)
return;
- Keyring *k = Keyring::instance();
- QStringList accounts = k->getStoredAccounts("minecraft");
-
- if (!accounts.isEmpty())
+ // Find an account to use.
+ std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
+ MojangAccountPtr account = accounts->activeAccount();
+ if (accounts->count() <= 0)
{
- QString username = accounts[0];
- QString password = k->getPassword("minecraft", username);
+ // Tell the user they need to log in at least one account in order to play.
+ auto reply = CustomMessageBox::selectable(this, tr("No Accounts"),
+ tr("In order to play Minecraft, you must have at least one Mojang or Minecraft account logged in to MultiMC."
+ "Would you like to open the account manager to add an account now?"),
+ QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec();
- if (!password.isEmpty())
- {
- QLOG_INFO() << "Automatically logging in with stored account: " << username;
- m_activeInst = m_selectedInstance;
- doLogin(username, password);
- }
- else
+ if (reply == QMessageBox::Yes)
{
- QLOG_ERROR() << "Auto login set for account, but no password was found: "
- << username;
- doLogin(tr("Auto login attempted, but no password is stored."));
+ // Open the account manager.
+ on_actionManageAccounts_triggered();
}
}
- else
+ else if (account.get() == nullptr)
{
- QLOG_ERROR() << "Auto login set but no accounts were stored.";
- doLogin(tr("Auto login attempted, but no accounts are stored."));
- }
-}
-
-void MainWindow::doLogin(QString username, QString password)
-{
- PasswordLogin uInfo{username, password};
-
- ProgressDialog *tDialog = new ProgressDialog(this);
- LoginTask *loginTask = new LoginTask(uInfo, tDialog);
- connect(loginTask, SIGNAL(succeeded()), SLOT(onLoginComplete()), Qt::QueuedConnection);
- connect(loginTask, SIGNAL(failed(QString)), SLOT(doLogin(QString)), Qt::QueuedConnection);
-
- tDialog->exec(loginTask);
-}
-
-void MainWindow::doLogin(const QString &errorMsg)
-{
- if (!m_selectedInstance)
- return;
+ // Tell the user they need to log in at least one account in order to play.
+ auto reply = CustomMessageBox::selectable(this, tr("No Account Selected"),
+ tr("You don't have an account selected as an active account."
+ "Would you like to open the account manager to select one now?"),
+ QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec();
- LoginDialog *loginDlg = new LoginDialog(this, errorMsg);
- if (!m_selectedInstance->lastLaunch())
- loginDlg->forceOnline();
-
- loginDlg->exec();
- if (loginDlg->result() == QDialog::Accepted)
- {
- if (loginDlg->isOnline())
+ if (reply == QMessageBox::Yes)
{
- m_activeInst = m_selectedInstance;
- doLogin(loginDlg->getUsername(), loginDlg->getPassword());
- }
- else
- {
- QString user = loginDlg->getUsername();
- if (user.length() == 0)
- user = QString("Player");
- m_activeLogin = {user, QString("Offline"), user, QString()};
- m_activeInst = m_selectedInstance;
- launchInstance(m_activeInst, m_activeLogin);
+ // Open the account manager.
+ on_actionManageAccounts_triggered();
}
}
+ else
+ {
+ // We'll need to validate the access token to make sure the account is still logged in.
+ // TODO: Do that ^
+
+ prepareLaunch(m_selectedInstance, account);
+ }
}
-void MainWindow::onLoginComplete()
+void MainWindow::prepareLaunch(BaseInstance* instance, MojangAccountPtr account)
{
- if (!m_activeInst)
- return;
- LoginTask *task = (LoginTask *)QObject::sender();
- m_activeLogin = task->getResult();
-
- Task *updateTask = m_activeInst->doUpdate();
+ Task *updateTask = instance->doUpdate();
if (!updateTask)
{
- launchInstance(m_activeInst, m_activeLogin);
+ launchInstance(instance, account);
}
else
{
ProgressDialog tDialog(this);
- connect(updateTask, SIGNAL(succeeded()), SLOT(onGameUpdateComplete()));
+ connect(updateTask, &Task::succeeded, [this, instance, account] { launchInstance(instance, account); });
connect(updateTask, SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
tDialog.exec(updateTask);
delete updateTask;
}
- auto job = new NetJob("Player skin: " + m_activeLogin.player_name);
+ QString playerName = account->currentProfile()->name();
- auto meta = MMC->metacache()->resolveEntry("skins", m_activeLogin.player_name + ".png");
+ auto job = new NetJob("Player skin: " + playerName);
+
+ auto meta = MMC->metacache()->resolveEntry("skins", playerName + ".png");
auto action = CacheDownload::make(
- QUrl("http://skins.minecraft.net/MinecraftSkins/" + m_activeLogin.player_name + ".png"),
+ QUrl("http://skins.minecraft.net/MinecraftSkins/" + playerName + ".png"),
meta);
job->addNetAction(action);
meta->stale = true;
@@ -678,12 +644,12 @@ void MainWindow::onLoginComplete()
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
QJsonObject root = jsonDoc.object();
QJsonObject mappings = root.value("mappings").toObject();
- QJsonArray usernames = mappings.value(m_activeLogin.username).toArray();
+ QJsonArray usernames = mappings.value(account->username()).toArray();
- if (!usernames.contains(m_activeLogin.player_name))
+ if (!usernames.contains(playerName))
{
- usernames.prepend(m_activeLogin.player_name);
- mappings[m_activeLogin.username] = usernames;
+ usernames.prepend(playerName);
+ mappings[account->username()] = usernames;
root["mappings"] = mappings;
jsonDoc.setObject(root);
@@ -693,22 +659,12 @@ void MainWindow::onLoginComplete()
}
}
-void MainWindow::onGameUpdateComplete()
-{
- launchInstance(m_activeInst, m_activeLogin);
-}
-
-void MainWindow::onGameUpdateError(QString error)
-{
- CustomMessageBox::selectable(this, tr("Error updating instance"), error,
- QMessageBox::Warning)->show();
-}
-
-void MainWindow::launchInstance(BaseInstance *instance, LoginResponse response)
+void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account)
{
Q_ASSERT_X(instance != NULL, "launchInstance", "instance is NULL");
+ Q_ASSERT_X(account.get() != nullptr, "launchInstance", "account is NULL");
- proc = instance->prepareForLaunch(response);
+ proc = instance->prepareForLaunch(account);
if (!proc)
return;
@@ -717,10 +673,19 @@ void MainWindow::launchInstance(BaseInstance *instance, LoginResponse response)
console = new ConsoleWindow(proc);
connect(console, SIGNAL(isClosing()), this, SLOT(instanceEnded()));
- proc->setLogin(response.username, response.session_id);
+ // I think this will work...
+ QString username = account->username();
+ QString session_id = account->accessToken();
+ proc->setLogin(username, session_id);
proc->launch();
}
+void MainWindow::onGameUpdateError(QString error)
+{
+ CustomMessageBox::selectable(this, tr("Error updating instance"), error,
+ QMessageBox::Warning)->show();
+}
+
void MainWindow::taskStart()
{
// Nothing to do here yet.
diff --git a/gui/MainWindow.h b/gui/MainWindow.h
index b1678f76..3b8d4668 100644
--- a/gui/MainWindow.h
+++ b/gui/MainWindow.h
@@ -22,6 +22,8 @@
#include "logic/net/LoginTask.h"
#include "logic/BaseInstance.h"
+#include "logic/auth/MojangAccount.h"
+
class QToolButton;
class LabeledToolButton;
class QLabel;
@@ -80,6 +82,8 @@ slots:
void on_actionSettings_triggered();
+ void on_actionManageAccounts_triggered();
+
void on_actionReportBug_triggered();
void on_actionNews_triggered();
@@ -103,12 +107,18 @@ slots:
void on_actionEditInstNotes_triggered();
void doLogin(const QString &errorMsg = "");
- void doLogin(QString username, QString password);
- void doAutoLogin();
- void onLoginComplete();
+ /*!
+ * Launches the given instance with the given account.
+ * This function assumes that the given account has a valid, usable access token.
+ */
+ void launchInstance(BaseInstance* instance, MojangAccountPtr account);
+
+ /*!
+ * Prepares the given instance for launch with the given account.
+ */
+ void prepareLaunch(BaseInstance* instance, MojangAccountPtr account);
- void onGameUpdateComplete();
void onGameUpdateError(QString error);
void taskStart();
@@ -136,8 +146,6 @@ slots:
void startTask(Task *task);
- void launchInstance(BaseInstance *inst, LoginResponse response);
-
protected:
bool eventFilter(QObject *obj, QEvent *ev);
void setCatBackground(bool enabled);
@@ -155,12 +163,6 @@ private:
BaseInstance *m_selectedInstance;
- // A pointer to the instance we are actively doing stuff with.
- // This is set when the user launches an instance and is used to refer to that
- // instance throughout the launching process.
- BaseInstance *m_activeInst;
- LoginResponse m_activeLogin;
-
Task *m_versionLoadTask;
QLabel *m_statusLeft;
diff --git a/gui/MainWindow.ui b/gui/MainWindow.ui
index 6f70fc98..f76d4d4e 100644
--- a/gui/MainWindow.ui
+++ b/gui/MainWindow.ui
@@ -70,6 +70,7 @@
<addaction name="separator"/>
<addaction name="actionCheckUpdate"/>
<addaction name="actionSettings"/>
+ <addaction name="actionManageAccounts"/>
<addaction name="separator"/>
<addaction name="actionReportBug"/>
<addaction name="actionNews"/>
@@ -465,6 +466,14 @@
<string>Add a new instance.</string>
</property>
</action>
+ <action name="actionManageAccounts">
+ <property name="text">
+ <string>Manage Accounts</string>
+ </property>
+ <property name="toolTip">
+ <string>Manage your Mojang or Minecraft accounts.</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
diff --git a/gui/dialogs/AccountListDialog.cpp b/gui/dialogs/AccountListDialog.cpp
new file mode 100644
index 00000000..c685c164
--- /dev/null
+++ b/gui/dialogs/AccountListDialog.cpp
@@ -0,0 +1,112 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AccountListDialog.h"
+#include "ui_AccountListDialog.h"
+
+#include <QItemSelectionModel>
+
+#include <logger/QsLog.h>
+
+#include <logic/auth/AuthenticateTask.h>
+
+#include <gui/dialogs/LoginDialog.h>
+#include <gui/dialogs/ProgressDialog.h>
+
+#include <MultiMC.h>
+
+AccountListDialog::AccountListDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::AccountListDialog)
+{
+ ui->setupUi(this);
+
+ m_accounts = MMC->accounts();
+ // TODO: Make the "Active?" column show checkboxes or radio buttons.
+ ui->listView->setModel(m_accounts.get());
+}
+
+AccountListDialog::~AccountListDialog()
+{
+ delete ui;
+}
+
+
+void AccountListDialog::on_addAccountBtn_clicked()
+{
+ doLogin("Please log in to add your account.");
+}
+
+void AccountListDialog::on_rmAccountBtn_clicked()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ m_accounts->removeAccount(selected);
+ }
+}
+
+void AccountListDialog::on_editAccountBtn_clicked()
+{
+ // TODO
+}
+
+void AccountListDialog::on_setActiveBtn_clicked()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ m_accounts->setActiveAccount(account->username());
+ }
+}
+
+void AccountListDialog::on_closeBtnBox_rejected()
+{
+ close();
+}
+
+void AccountListDialog::doLogin(const QString& errMsg)
+{
+ // TODO: We can use the login dialog for this for now, but we'll have to make something better for it eventually.
+ LoginDialog loginDialog(this);
+ loginDialog.exec();
+
+ if (loginDialog.result() == QDialog::Accepted)
+ {
+ QString username(loginDialog.getUsername());
+ QString password(loginDialog.getPassword());
+
+ MojangAccountPtr account = MojangAccountPtr(new MojangAccount(username));
+
+ ProgressDialog* progDialog = new ProgressDialog(this);
+ m_authTask = new AuthenticateTask(account, password, progDialog);
+ connect(m_authTask, SIGNAL(succeeded()), SLOT(onLoginComplete()), Qt::QueuedConnection);
+ connect(m_authTask, SIGNAL(failed(QString)), SLOT(doLogin(QString)), Qt::QueuedConnection);
+ progDialog->exec(m_authTask);
+ //delete m_authTask;
+ }
+}
+
+void AccountListDialog::onLoginComplete()
+{
+ // Add the authenticated account to the accounts list.
+ MojangAccountPtr account = m_authTask->getMojangAccount();
+ m_accounts->addAccount(account);
+ //ui->listView->update();
+}
+
diff --git a/gui/dialogs/AccountListDialog.h b/gui/dialogs/AccountListDialog.h
new file mode 100644
index 00000000..17a50bec
--- /dev/null
+++ b/gui/dialogs/AccountListDialog.h
@@ -0,0 +1,63 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include <memory>
+
+#include "logic/lists/MojangAccountList.h"
+
+namespace Ui {
+class AccountListDialog;
+}
+
+class AuthenticateTask;
+
+class AccountListDialog : public QDialog
+{
+Q_OBJECT
+public:
+ explicit AccountListDialog(QWidget *parent = 0);
+ ~AccountListDialog();
+
+public
+slots:
+ void on_addAccountBtn_clicked();
+
+ void on_rmAccountBtn_clicked();
+
+ void on_editAccountBtn_clicked();
+
+ void on_setActiveBtn_clicked();
+
+ // This will be sent when the "close" button is clicked.
+ void on_closeBtnBox_rejected();
+
+protected:
+ std::shared_ptr<MojangAccountList> m_accounts;
+
+ AuthenticateTask* m_authTask;
+
+protected
+slots:
+ void doLogin(const QString& errMsg="");
+ void onLoginComplete();
+
+private:
+ Ui::AccountListDialog *ui;
+};
+
diff --git a/gui/dialogs/AccountListDialog.ui b/gui/dialogs/AccountListDialog.ui
new file mode 100644
index 00000000..2872b368
--- /dev/null
+++ b/gui/dialogs/AccountListDialog.ui
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AccountListDialog</class>
+ <widget class="QDialog" name="AccountListDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Dialog</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QLabel" name="welcomeLabel">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Welcome! If you're new here, you can click the &amp;quot;Add&amp;quot; button to add your Mojang or Minecraft account.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QTreeView" name="listView"/>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="manageAcctsBtnBox">
+ <item>
+ <widget class="QPushButton" name="addAccountBtn">
+ <property name="text">
+ <string>&amp;Add</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="editAccountBtn">
+ <property name="text">
+ <string>&amp;Edit</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="rmAccountBtn">
+ <property name="text">
+ <string>&amp;Remove</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="buttonSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="setActiveBtn">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the currently selected account as the active account. The active account is the account that is used to log in (unless it is overridden in an instance-specific setting).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>&amp;Set Active</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="closeBtnBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h
index 6c9b16c7..cf86fda6 100644
--- a/logic/BaseInstance.h
+++ b/logic/BaseInstance.h
@@ -22,7 +22,7 @@
#include "inifile.h"
#include "lists/BaseVersionList.h"
-#include "net/LoginTask.h"
+#include "logic/auth/MojangAccount.h"
class QDialog;
class Task;
@@ -153,8 +153,8 @@ public:
/// returns a valid update task if update is needed, NULL otherwise
virtual Task *doUpdate() = 0;
- /// returns a valid minecraft process, ready for launch
- virtual MinecraftProcess *prepareForLaunch(LoginResponse response) = 0;
+ /// returns a valid minecraft process, ready for launch with the given account.
+ virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) = 0;
/// do any necessary cleanups after the instance finishes. also runs before
/// 'prepareForLaunch'
diff --git a/logic/InstanceLauncher.cpp b/logic/InstanceLauncher.cpp
index 534d07d1..0ef0f045 100644
--- a/logic/InstanceLauncher.cpp
+++ b/logic/InstanceLauncher.cpp
@@ -38,6 +38,8 @@ void InstanceLauncher::onTerminated()
void InstanceLauncher::onLoginComplete()
{
+ // TODO: Fix this.
+ /*
LoginTask *task = (LoginTask *)QObject::sender();
auto result = task->getResult();
auto instance = MMC->instances()->getInstanceById(instId);
@@ -52,6 +54,7 @@ void InstanceLauncher::onLoginComplete()
proc->setLogin(result.username, result.session_id);
proc->launch();
+ */
}
void InstanceLauncher::doLogin(const QString &errorMsg)
diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp
index 07e302f5..ab6536d0 100644
--- a/logic/LegacyInstance.cpp
+++ b/logic/LegacyInstance.cpp
@@ -50,7 +50,7 @@ Task *LegacyInstance::doUpdate()
return new LegacyUpdate(this, this);
}
-MinecraftProcess *LegacyInstance::prepareForLaunch(LoginResponse response)
+MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account)
{
MinecraftProcess *proc = new MinecraftProcess(this);
@@ -103,8 +103,8 @@ MinecraftProcess *LegacyInstance::prepareForLaunch(LoginResponse response)
#endif
args << "-jar" << LAUNCHER_FILE;
- args << response.player_name;
- args << response.session_id;
+ args << account->currentProfile()->name();
+ args << account->sessionId();
args << windowTitle;
args << windowSize;
args << lwjgl;
diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h
index 04b16b9b..3d35521e 100644
--- a/logic/LegacyInstance.h
+++ b/logic/LegacyInstance.h
@@ -80,7 +80,7 @@ public:
virtual void setShouldUpdate(bool val);
virtual Task *doUpdate();
- virtual MinecraftProcess *prepareForLaunch(LoginResponse response);
+ virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account);
virtual void cleanupAfterRun();
virtual QDialog *createModEditDialog(QWidget *parent);
@@ -93,4 +93,4 @@ public:
protected
slots:
virtual void jarModsChanged();
-}; \ No newline at end of file
+};
diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp
index 7764d225..a947b7c0 100644
--- a/logic/OneSixInstance.cpp
+++ b/logic/OneSixInstance.cpp
@@ -66,7 +66,7 @@ QString replaceTokensIn(QString text, QMap<QString, QString> with)
return result;
}
-QStringList OneSixInstance::processMinecraftArgs(LoginResponse response)
+QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account)
{
I_D(OneSixInstance);
auto version = d->version;
@@ -74,11 +74,11 @@ QStringList OneSixInstance::processMinecraftArgs(LoginResponse response)
QMap<QString, QString> token_mapping;
// yggdrasil!
- token_mapping["auth_username"] = response.username;
- token_mapping["auth_session"] = response.session_id;
- token_mapping["auth_access_token"] = response.access_token;
- token_mapping["auth_player_name"] = response.player_name;
- token_mapping["auth_uuid"] = response.player_id;
+ token_mapping["auth_username"] = account->username();
+ token_mapping["auth_session"] = account->sessionId();
+ token_mapping["auth_access_token"] = account->accessToken();
+ token_mapping["auth_player_name"] = account->currentProfile()->name();
+ token_mapping["auth_uuid"] = account->currentProfile()->id();
// this is for offline?:
/*
@@ -105,7 +105,7 @@ QStringList OneSixInstance::processMinecraftArgs(LoginResponse response)
return parts;
}
-MinecraftProcess *OneSixInstance::prepareForLaunch(LoginResponse response)
+MinecraftProcess *OneSixInstance::prepareForLaunch(MojangAccountPtr account)
{
I_D(OneSixInstance);
cleanupAfterRun();
@@ -177,7 +177,7 @@ MinecraftProcess *OneSixInstance::prepareForLaunch(LoginResponse response)
args << classPath;
}
args << version->mainClass;
- args.append(processMinecraftArgs(response));
+ args.append(processMinecraftArgs(account));
// Set the width and height for 1.6 instances
bool maximize = settings().get("LaunchMaximized").toBool();
diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h
index 2d44ddba..e30ca7ca 100644
--- a/logic/OneSixInstance.h
+++ b/logic/OneSixInstance.h
@@ -40,7 +40,8 @@ public:
virtual QString instanceConfigFolder() const;
virtual Task *doUpdate();
- virtual MinecraftProcess *prepareForLaunch(LoginResponse response);
+ virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account);
+
virtual void cleanupAfterRun();
virtual QString intendedVersionId() const;
@@ -72,5 +73,5 @@ public:
virtual QString getStatusbarDescription();
private:
- QStringList processMinecraftArgs(LoginResponse response);
-}; \ No newline at end of file
+ QStringList processMinecraftArgs(MojangAccountPtr account);
+};
diff --git a/logic/auth/AuthenticateTask.cpp b/logic/auth/AuthenticateTask.cpp
new file mode 100644
index 00000000..a9c2c03f
--- /dev/null
+++ b/logic/auth/AuthenticateTask.cpp
@@ -0,0 +1,180 @@
+
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <logic/auth/AuthenticateTask.h>
+
+#include <logic/auth/MojangAccount.h>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+#include <QDebug>
+
+#include "logger/QsLog.h"
+
+AuthenticateTask::AuthenticateTask(MojangAccountPtr 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
+ * }
+ */
+ 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", getMojangAccount()->username());
+ req.insert("password", m_password);
+
+ // If we already have a client token, give it to the server.
+ // Otherwise, let the server give us one.
+ if (!getMojangAccount()->clientToken().isEmpty())
+ req.insert("clientToken", getMojangAccount()->clientToken());
+
+ return req;
+}
+
+bool AuthenticateTask::processResponse(QJsonObject responseData)
+{
+ // Read the response data. We need to get the client token, access token, and the selected profile.
+ QLOG_DEBUG() << "Processing authentication response.";
+
+ // If we already have a client token, make sure the one the server gave us matches our existing one.
+ QLOG_DEBUG() << "Getting client token.";
+ QString clientToken = responseData.value("clientToken").toString("");
+ if (clientToken.isEmpty())
+ {
+ // Fail if the server gave us an empty client token
+ // TODO: Set an error properly to display to the user.
+ QLOG_ERROR() << "Server didn't send a client token.";
+ return false;
+ }
+ if (!getMojangAccount()->clientToken().isEmpty() && clientToken != getMojangAccount()->clientToken())
+ {
+ // The server changed our client token! Obey its wishes, but complain. That's what I do for my parents, so...
+ QLOG_WARN() << "Server changed our client token to '" << clientToken
+ << "'. This shouldn't happen, but it isn't really a big deal.";
+ }
+ // Set the client token.
+ getMojangAccount()->setClientToken(clientToken);
+
+
+ // Now, we set the access token.
+ QLOG_DEBUG() << "Getting access token.";
+ QString accessToken = responseData.value("accessToken").toString("");
+ if (accessToken.isEmpty())
+ {
+ // Fail if the server didn't give us an access token.
+ // TODO: Set an error properly to display to the user.
+ QLOG_ERROR() << "Server didn't send an access token.";
+ }
+ // Set the access token.
+ getMojangAccount()->setAccessToken(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.
+ QLOG_DEBUG() << "Loading profile list.";
+ QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
+ ProfileList 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("");
+
+ 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.
+ QLOG_WARN() << "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(AccountProfile(id, name));
+ }
+ // Put the list of profiles we loaded into the MojangAccount object.
+ getMojangAccount()->loadProfiles(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).
+ QLOG_DEBUG() << "Setting current profile.";
+ QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
+ QString currentProfileId = currentProfile.value("id").toString("");
+ if (currentProfileId.isEmpty())
+ {
+ // TODO: Set an error to display to the user.
+ QLOG_ERROR() << "Server didn't specify a currently selected profile.";
+ return false;
+ }
+ if (!getMojangAccount()->setProfile(currentProfileId))
+ {
+ // TODO: Set an error to display to the user.
+ QLOG_ERROR() << "Server specified a selected profile that wasn't in the available profiles list.";
+ return false;
+ }
+
+
+ // We've made it through the minefield of possible errors. Return true to indicate that we've succeeded.
+ QLOG_DEBUG() << "Finished reading authentication response.";
+ return true;
+}
+
+QString AuthenticateTask::getEndpoint() const
+{
+ return "authenticate";
+}
+
+QString AuthenticateTask::getStateMessage(const YggdrasilTask::State state) const
+{
+ switch (state)
+ {
+ case STATE_SENDING_REQUEST:
+ return tr("Authenticating: Sending request.");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Authenticating: Processing response.");
+ default:
+ return YggdrasilTask::getStateMessage(state);
+ }
+}
+
diff --git a/logic/auth/AuthenticateTask.h b/logic/auth/AuthenticateTask.h
new file mode 100644
index 00000000..54a6b79a
--- /dev/null
+++ b/logic/auth/AuthenticateTask.h
@@ -0,0 +1,46 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <logic/auth/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(MojangAccountPtr account, const QString& password, QObject* parent=0);
+
+protected:
+ virtual QJsonObject getRequestContent() const;
+
+ virtual QString getEndpoint() const;
+
+ virtual bool processResponse(QJsonObject responseData);
+
+ QString getStateMessage(const YggdrasilTask::State state) const;
+
+private:
+ QString m_password;
+};
+
diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp
new file mode 100644
index 00000000..4f3839bc
--- /dev/null
+++ b/logic/auth/MojangAccount.cpp
@@ -0,0 +1,220 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MojangAccount.h"
+
+#include <QUuid>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include <logger/QsLog.h>
+
+MojangAccount::MojangAccount(const QString& username, QObject* parent) :
+ QObject(parent)
+{
+ // Generate a client token.
+ m_clientToken = QUuid::createUuid().toString();
+
+ m_username = username;
+
+ m_currentProfile = -1;
+}
+
+MojangAccount::MojangAccount(const QString& username, const QString& clientToken,
+ const QString& accessToken, QObject* parent) :
+ QObject(parent)
+{
+ m_username = username;
+ m_clientToken = clientToken;
+ m_accessToken = accessToken;
+
+ m_currentProfile = -1;
+}
+
+MojangAccount::MojangAccount(const MojangAccount& other, QObject* parent)
+{
+ m_username = other.username();
+ m_clientToken = other.clientToken();
+ m_accessToken = other.accessToken();
+
+ m_profiles = other.m_profiles;
+ m_currentProfile = other.m_currentProfile;
+}
+
+
+QString MojangAccount::username() const
+{
+ return m_username;
+}
+
+QString MojangAccount::clientToken() const
+{
+ return m_clientToken;
+}
+
+void MojangAccount::setClientToken(const QString& clientToken)
+{
+ m_clientToken = clientToken;
+}
+
+
+QString MojangAccount::accessToken() const
+{
+ return m_accessToken;
+}
+
+void MojangAccount::setAccessToken(const QString& accessToken)
+{
+ m_accessToken = accessToken;
+}
+
+QString MojangAccount::sessionId() const
+{
+ return "token:" + m_accessToken + ":" + currentProfile()->id();
+}
+
+const QList<AccountProfile> MojangAccount::profiles() const
+{
+ return m_profiles;
+}
+
+const AccountProfile* MojangAccount::currentProfile() const
+{
+ if (m_currentProfile < 0)
+ {
+ if (m_profiles.length() > 0)
+ return &m_profiles.at(0);
+ else
+ return nullptr;
+ }
+ else
+ return &m_profiles.at(m_currentProfile);
+}
+
+bool MojangAccount::setProfile(const QString& profileId)
+{
+ const QList<AccountProfile>& profiles = this->profiles();
+ for (int i = 0; i < profiles.length(); i++)
+ {
+ if (profiles.at(i).id() == profileId)
+ {
+ m_currentProfile = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+void MojangAccount::loadProfiles(const ProfileList& profiles)
+{
+ m_profiles.clear();
+ for (auto profile : profiles)
+ m_profiles.append(profile);
+}
+
+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())
+ {
+ QLOG_ERROR() << "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)
+ {
+ QLOG_ERROR() << "Can't load Mojang account with username \"" << username << "\". No profiles found.";
+ return nullptr;
+ }
+
+ ProfileList profiles;
+ for (QJsonValue profileVal : profileArray)
+ {
+ QJsonObject profileObject = profileVal.toObject();
+ QString id = profileObject.value("id").toString("");
+ QString name = profileObject.value("name").toString("");
+ if (id.isEmpty() || name.isEmpty())
+ {
+ QLOG_WARN() << "Unable to load a profile because it was missing an ID or a name.";
+ continue;
+ }
+ profiles.append(AccountProfile(id, name));
+ }
+
+ MojangAccountPtr account(new MojangAccount(username, clientToken, accessToken));
+ account->loadProfiles(profiles);
+
+ // Get the currently selected profile.
+ QString currentProfile = object.value("activeProfile").toString("");
+ if (!currentProfile.isEmpty())
+ account->setProfile(currentProfile);
+
+ return account;
+}
+
+QJsonObject MojangAccount::saveToJson()
+{
+ QJsonObject json;
+ json.insert("username", username());
+ json.insert("clientToken", clientToken());
+ json.insert("accessToken", accessToken());
+
+ QJsonArray profileArray;
+ for (AccountProfile profile : m_profiles)
+ {
+ QJsonObject profileObj;
+ profileObj.insert("id", profile.id());
+ profileObj.insert("name", profile.name());
+ profileArray.append(profileObj);
+ }
+ json.insert("profiles", profileArray);
+
+ if (currentProfile() != nullptr)
+ json.insert("activeProfile", currentProfile()->id());
+
+ return json;
+}
+
+
+AccountProfile::AccountProfile(const QString& id, const QString& name)
+{
+ m_id = id;
+ m_name = name;
+}
+
+AccountProfile::AccountProfile(const AccountProfile& other)
+{
+ m_id = other.m_id;
+ m_name = other.m_name;
+}
+
+QString AccountProfile::id() const
+{
+ return m_id;
+}
+
+QString AccountProfile::name() const
+{
+ return m_name;
+}
+
+
diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h
new file mode 100644
index 00000000..062b8aa2
--- /dev/null
+++ b/logic/auth/MojangAccount.h
@@ -0,0 +1,154 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QList>
+#include <QJsonObject>
+
+#include <memory>
+
+class MojangAccount;
+
+typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
+Q_DECLARE_METATYPE(MojangAccountPtr)
+
+
+/**
+ * Class that represents 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.
+ */
+class AccountProfile
+{
+public:
+ AccountProfile(const QString& id, const QString& name);
+ AccountProfile(const AccountProfile& other);
+
+ QString id() const;
+ QString name() const;
+protected:
+ QString m_id;
+ QString m_name;
+};
+
+
+typedef QList<AccountProfile> ProfileList;
+
+
+/**
+ * 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 MojangAccount : public QObject
+{
+Q_OBJECT
+public:
+ /**
+ * Constructs a new MojangAccount with the given username.
+ * The client token will be generated automatically and the access token will be blank.
+ */
+ explicit MojangAccount(const QString& username, QObject* parent = 0);
+
+ /**
+ * Constructs a new MojangAccount with the given username, client token, and access token.
+ */
+ explicit MojangAccount(const QString& username, const QString& clientToken, const QString& accessToken, QObject* parent = 0);
+
+ /**
+ * Constructs a new MojangAccount matching the given account.
+ */
+ MojangAccount(const MojangAccount& other, QObject* parent);
+
+ /**
+ * 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();
+
+
+ /**
+ * This MojangAccount's username. May be an email address if the account is migrated.
+ */
+ QString username() const;
+
+ /**
+ * This MojangAccount's client token. This is a UUID used by Mojang's auth servers to identify this client.
+ * This is unique for each MojangAccount.
+ */
+ QString clientToken() const;
+
+ /**
+ * Sets the MojangAccount's client token to the given value.
+ */
+ void setClientToken(const QString& token);
+
+ /**
+ * This MojangAccount's access token.
+ * If the user has not chosen to stay logged in, this will be an empty string.
+ */
+ QString accessToken() const;
+
+ /**
+ * Changes this MojangAccount's access token to the given value.
+ */
+ void setAccessToken(const QString& token);
+
+ /**
+ * Get full session ID
+ */
+ QString sessionId() const;
+
+ /**
+ * Returns a list of the available account profiles.
+ */
+ const ProfileList profiles() const;
+
+ /**
+ * Returns a pointer to the currently selected profile.
+ * If no profile is selected, returns the first profile in the profile list or nullptr if there are none.
+ */
+ const AccountProfile* currentProfile() const;
+
+ /**
+ * 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 setProfile(const QString& profileId);
+
+ /**
+ * Clears the current account profile list and replaces it with the given profile list.
+ */
+ void loadProfiles(const ProfileList& profiles);
+
+
+protected:
+ QString m_username;
+ QString m_clientToken;
+ QString m_accessToken; // Blank if not logged in.
+ int m_currentProfile; // Index of the selected profile within the list of available profiles. -1 if nothing is selected.
+ ProfileList m_profiles; // List of available profiles.
+};
+
diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp
new file mode 100644
index 00000000..39dfb749
--- /dev/null
+++ b/logic/auth/YggdrasilTask.cpp
@@ -0,0 +1,182 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <logic/auth/YggdrasilTask.h>
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QByteArray>
+
+#include <MultiMC.h>
+#include <logic/auth/MojangAccount.h>
+
+YggdrasilTask::YggdrasilTask(MojangAccountPtr account, QObject* parent) : Task(parent)
+{
+ m_error = nullptr;
+ m_account = account;
+}
+
+
+YggdrasilTask::~YggdrasilTask()
+{
+ if (m_error)
+ delete m_error;
+}
+
+void YggdrasilTask::executeTask()
+{
+ setStatus(getStateMessage(STATE_SENDING_REQUEST));
+
+ // Get the content of the request we're going to send to the server.
+ QJsonDocument doc(getRequestContent());
+
+ auto worker = MMC->qnam();
+ connect(worker.get(), SIGNAL(finished(QNetworkReply*)), this,
+ SLOT(processReply(QNetworkReply*)));
+
+ QUrl reqUrl("https://authserver.mojang.com/" + getEndpoint());
+ QNetworkRequest netRequest(reqUrl);
+ netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ m_netReply = worker->post(netRequest, doc.toJson());
+}
+
+void YggdrasilTask::processReply(QNetworkReply* reply)
+{
+ setStatus(getStateMessage(STATE_PROCESSING_RESPONSE));
+
+ if (m_netReply != reply)
+ // Wrong reply for some reason...
+ return;
+
+ // Check for errors.
+ switch (reply->error())
+ {
+ case QNetworkReply::NoError:
+ {
+ // 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 = reply->readAll();
+ QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
+
+ // Check the response code.
+ int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ switch (responseCode)
+ {
+ case 200:
+ {
+ // If the response code was 200, then there shouldn't be an error. Make sure anyways.
+ switch (jsonError.error)
+ {
+ case QJsonParseError::NoError:
+ if (!processResponse(doc.object()))
+ {
+ YggdrasilTask::Error* err = getError();
+ if (err)
+ emitFailed(err->getErrorMessage());
+ else
+ emitFailed(tr("An unknown error occurred when processing the response from the authentication server."));
+ }
+ else
+ {
+ emitSucceeded();
+ }
+ break;
+
+ default:
+ emitFailed(tr("Failed to parse Yggdrasil JSON response: \"%1\".").arg(jsonError.errorString()));
+ break;
+ }
+ break;
+ }
+
+ default:
+ // If the response code was something else, 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.
+ switch (jsonError.error)
+ {
+ case 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.
+ processError(doc.object());
+ break;
+
+ default:
+ // The server didn't say anything regarding the error. Give the user an unknown error.
+ emitFailed(tr("Login failed: Unknown HTTP code %1 encountered.").arg(responseCode));
+ break;
+ }
+ break;
+ }
+
+ break;
+ }
+
+ case QNetworkReply::OperationCanceledError:
+ emitFailed(tr("Login canceled."));
+ break;
+
+ default:
+ emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server."));
+ break;
+ }
+}
+
+QString YggdrasilTask::processError(QJsonObject responseData)
+{
+ QJsonValue errorVal = responseData.value("error");
+ QJsonValue msgVal = responseData.value("errorMessage");
+ QJsonValue causeVal = responseData.value("cause");
+
+ if (errorVal.isString() && msgVal.isString() && causeVal.isString())
+ {
+ m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString(""));
+ return m_error->getDisplayMessage();
+ }
+ else
+ {
+ // Error is not in standard format. Don't set m_error and return unknown error.
+ return tr("An unknown Yggdrasil error occurred.");
+ }
+}
+
+QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const
+{
+ switch (state)
+ {
+ case STATE_SENDING_REQUEST:
+ return tr("Sending request to auth servers.");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Processing response from servers.");
+ default:
+ return tr("Processing. Please wait.");
+ }
+}
+
+YggdrasilTask::Error *YggdrasilTask::getError() const
+{
+ return this->m_error;
+}
+
+MojangAccountPtr YggdrasilTask::getMojangAccount() const
+{
+ return this->m_account;
+}
+
diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h
new file mode 100644
index 00000000..6aebae16
--- /dev/null
+++ b/logic/auth/YggdrasilTask.h
@@ -0,0 +1,128 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <logic/tasks/Task.h>
+
+#include <QString>
+#include <QJsonObject>
+
+#include "logic/auth/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(MojangAccountPtr account, QObject* parent=0);
+ ~YggdrasilTask();
+
+ /**
+ * Class describing a Yggdrasil error response.
+ */
+ class Error
+ {
+ public:
+ Error(const QString& shortError, const QString& errorMessage, const QString& cause) :
+ m_shortError(shortError), m_errorMessage(errorMessage), m_cause(cause) {}
+
+ QString getShortError() const { return m_shortError; }
+ QString getErrorMessage() const { return m_errorMessage; }
+ QString getCause() const { return m_cause; }
+
+ /// Gets the string to display in the GUI for describing this error.
+ QString getDisplayMessage() { return getErrorMessage(); }
+
+ protected:
+ QString m_shortError;
+ QString m_errorMessage;
+ QString m_cause;
+ };
+
+ /**
+ * Gets the Mojang account that this task is operating on.
+ */
+ virtual MojangAccountPtr getMojangAccount() const;
+
+ /**
+ * Returns a pointer to a YggdrasilTask::Error object if an error has occurred.
+ * If no error has occurred, returns a null pointer.
+ */
+ virtual Error* getError() const;
+
+protected:
+ /**
+ * 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_SENDING_REQUEST,
+ STATE_PROCESSING_RESPONSE,
+ STATE_OTHER,
+ };
+
+ virtual void executeTask();
+
+ /**
+ * 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.
+ */
+ virtual bool 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 QString 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 State state) const;
+
+ MojangAccountPtr m_account;
+
+ QNetworkReply* m_netReply;
+
+ Error* m_error;
+
+protected slots:
+ void processReply(QNetworkReply* reply);
+};
+
diff --git a/logic/lists/MojangAccountList.cpp b/logic/lists/MojangAccountList.cpp
new file mode 100644
index 00000000..1d67c70f
--- /dev/null
+++ b/logic/lists/MojangAccountList.cpp
@@ -0,0 +1,349 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "logic/lists/MojangAccountList.h"
+
+#include <QIODevice>
+#include <QFile>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonParseError>
+
+#include "logger/QsLog.h"
+
+#include "logic/auth/MojangAccount.h"
+
+#define ACCOUNT_LIST_FORMAT_VERSION 1
+
+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();
+ 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
+{
+ if (m_activeAccount.isEmpty())
+ return nullptr;
+ else
+ return findAccount(m_activeAccount);
+}
+
+void MojangAccountList::setActiveAccount(const QString& username)
+{
+ beginResetModel();
+ for (MojangAccountPtr account : m_accounts)
+ if (account->username() == username)
+ m_activeAccount = username;
+ endResetModel();
+ onListChanged();
+}
+
+
+void MojangAccountList::onListChanged()
+{
+ if (m_autosave)
+ // TODO: Alert the user if this fails.
+ saveList();
+ emit listChanged();
+}
+
+
+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 ActiveColumn:
+ return account->username() == m_activeAccount;
+
+ case NameColumn:
+ return account->username();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return account->username();
+
+ case PointerRole:
+ return qVariantFromValue(account);
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return "Active?";
+
+ case NameColumn:
+ return "Name";
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case NameColumn:
+ return "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;
+}
+
+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())
+ {
+ QLOG_ERROR() << "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))
+ {
+ QLOG_ERROR() << 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)
+ {
+ QLOG_ERROR() << 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())
+ {
+ QLOG_ERROR() << "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";
+ QLOG_WARN() << "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)
+ {
+ m_accounts.append(account);
+ }
+ else
+ {
+ QLOG_WARN() << "Failed to load an account.";
+ }
+ }
+ endResetModel();
+
+ // Load the active account.
+ m_activeAccount = root.value("activeAccount").toString("");
+
+ return true;
+}
+
+bool MojangAccountList::saveList(const QString& filePath)
+{
+ QString path(filePath);
+ if (path.isEmpty()) path = m_listFilePath;
+ if (path.isEmpty())
+ {
+ QLOG_ERROR() << "Can't save Mojang account list. No file path given and no default set.";
+ return false;
+ }
+
+ QLOG_INFO() << "Writing account list to" << path;
+
+ QLOG_DEBUG() << "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.
+ QLOG_DEBUG() << "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);
+
+ // Save the active account.
+ root.insert("activeAccount", m_activeAccount);
+
+ // 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.
+ QLOG_DEBUG() << "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))
+ {
+ QLOG_ERROR() << 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.close();
+
+ QLOG_INFO() << "Saved account list to" << path;
+
+ return true;
+}
+
+void MojangAccountList::setListFilePath(QString path, bool autosave)
+{
+ m_listFilePath = path;
+ autosave = autosave;
+}
+
diff --git a/logic/lists/MojangAccountList.h b/logic/lists/MojangAccountList.h
new file mode 100644
index 00000000..71f472f7
--- /dev/null
+++ b/logic/lists/MojangAccountList.h
@@ -0,0 +1,175 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+#include "logic/auth/MojangAccount.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 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;
+
+ /*!
+ * 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.
+ */
+ virtual void setActiveAccount(const QString& username);
+
+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();
+
+protected:
+ /*!
+ * Called whenever the list changes.
+ * This emits the listChanged() signal and autosaves the list (if autosave is enabled).
+ */
+ void onListChanged();
+
+ QList<MojangAccountPtr> m_accounts;
+
+ /*!
+ * Username of the account that is currently active.
+ * Empty string if no account is active.
+ */
+ QString 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;
+
+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);
+};
+