From 94f3d61302f280678d465ef33e6faec1498ed579 Mon Sep 17 00:00:00 2001
From: 0xf8 <0xf8.dev@proton.me>
Date: Tue, 20 Jun 2023 14:35:02 -0400
Subject: [PATCH] Add custom login support
---
PollyMC.iml | 12 ++
launcher/CMakeLists.txt | 6 +
launcher/minecraft/MinecraftInstance.cpp | 6 +-
launcher/minecraft/auth/AccountData.cpp | 16 ++-
launcher/minecraft/auth/AccountData.h | 5 +-
launcher/minecraft/auth/AuthSession.h | 2 +
launcher/minecraft/auth/MinecraftAccount.cpp | 28 ++++
launcher/minecraft/auth/MinecraftAccount.h | 17 +++
launcher/minecraft/auth/Yggdrasil.cpp | 2 +
launcher/minecraft/auth/Yggdrasil.h | 2 +
launcher/minecraft/auth/flows/Custom.cpp | 25 ++++
launcher/minecraft/auth/flows/Custom.h | 27 ++++
.../auth/steps/CustomProfileStep.cpp | 94 +++++++++++++
.../minecraft/auth/steps/CustomProfileStep.h | 22 +++
launcher/minecraft/auth/steps/CustomStep.cpp | 52 +++++++
launcher/minecraft/auth/steps/CustomStep.h | 28 ++++
launcher/minecraft/launch/InjectAuthlib.cpp | 5 +-
launcher/minecraft/launch/InjectAuthlib.h | 1 +
launcher/ui/dialogs/CustomLoginDialog.cpp | 131 ++++++++++++++++++
launcher/ui/dialogs/CustomLoginDialog.h | 60 ++++++++
launcher/ui/dialogs/CustomLoginDialog.ui | 94 +++++++++++++
launcher/ui/pages/global/AccountListPage.cpp | 23 ++-
launcher/ui/pages/global/AccountListPage.h | 1 +
launcher/ui/pages/global/AccountListPage.ui | 6 +
24 files changed, 653 insertions(+), 12 deletions(-)
create mode 100644 PollyMC.iml
create mode 100644 launcher/minecraft/auth/flows/Custom.cpp
create mode 100644 launcher/minecraft/auth/flows/Custom.h
create mode 100644 launcher/minecraft/auth/steps/CustomProfileStep.cpp
create mode 100644 launcher/minecraft/auth/steps/CustomProfileStep.h
create mode 100644 launcher/minecraft/auth/steps/CustomStep.cpp
create mode 100644 launcher/minecraft/auth/steps/CustomStep.h
create mode 100644 launcher/ui/dialogs/CustomLoginDialog.cpp
create mode 100644 launcher/ui/dialogs/CustomLoginDialog.h
create mode 100644 launcher/ui/dialogs/CustomLoginDialog.ui
diff --git a/PollyMC.iml b/PollyMC.iml
new file mode 100644
index 00000000..03682804
--- /dev/null
+++ b/PollyMC.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 1a2383e9..eea16924 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -222,6 +222,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/flows/Offline.h
minecraft/auth/flows/Elyby.cpp
minecraft/auth/flows/Elyby.h
+ minecraft/auth/flows/Custom.cpp
+ minecraft/auth/flows/Custom.h
minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h
@@ -229,6 +231,10 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/ElybyProfileStep.h
minecraft/auth/steps/ElybyStep.cpp
minecraft/auth/steps/ElybyStep.h
+ minecraft/auth/steps/CustomProfileStep.cpp
+ minecraft/auth/steps/CustomProfileStep.h
+ minecraft/auth/steps/CustomStep.cpp
+ minecraft/auth/steps/CustomStep.h
minecraft/auth/steps/GetSkinStep.cpp
minecraft/auth/steps/GetSkinStep.h
minecraft/auth/steps/LauncherLoginStep.cpp
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 8f60510b..a42189c9 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -1031,7 +1031,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt
// authlib patch
if (session->user_type == "elyby")
{
- process->appendStep(makeShared(pptr, &m_injector));
+ process->appendStep(makeShared(pptr, &m_injector, "ely.by"));
+ }
+ else if (session->user_type == "custom")
+ {
+ process->appendStep(makeShared(pptr, &m_injector, session.url));
}
process->appendStep(makeShared(pptr, Net::Mode::Online));
diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp
index 50ca155f..c5aeee63 100644
--- a/launcher/minecraft/auth/AccountData.cpp
+++ b/launcher/minecraft/auth/AccountData.cpp
@@ -354,6 +354,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
type = AccountType::Offline;
} else if (typeS == "Elyby") {
type = AccountType::Elyby;
+ } else if (typeS == "Custom") {
+ type = AccountType::Custom
} else {
qWarning() << "Failed to parse account data: type is not recognized.";
return false;
@@ -414,6 +416,9 @@ QJsonObject AccountData::saveState() const {
else if (type == AccountType::Elyby) {
output["type"] = "Elyby";
}
+ else if (type == AccountType::Custom) {
+ output["type"] = "Custom";
+ }
tokenToJSONV3(output, yggdrasilToken, "ygg");
profileToJSONV3(output, minecraftProfile, "profile");
@@ -433,14 +438,14 @@ QString AccountData::accessToken() const {
}
QString AccountData::clientToken() const {
- if(type != AccountType::Mojang && type != AccountType::Elyby) {
+ if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) {
return QString();
}
return yggdrasilToken.extra["clientToken"].toString();
}
void AccountData::setClientToken(QString clientToken) {
- if(type != AccountType::Mojang && type != AccountType::Elyby) {
+ if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) {
return;
}
yggdrasilToken.extra["clientToken"] = clientToken;
@@ -454,7 +459,7 @@ void AccountData::generateClientTokenIfMissing() {
}
void AccountData::invalidateClientToken() {
- if(type != AccountType::Mojang && type != AccountType::Elyby) {
+ if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) {
return;
}
yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]"));
@@ -475,12 +480,11 @@ QString AccountData::profileName() const {
QString AccountData::accountDisplayString() const {
switch(type) {
+ case AccountType::Custom:
+ case AccountType::Elyby:
case AccountType::Mojang: {
return userName();
}
- case AccountType::Elyby: {
- return userName();
- }
case AccountType::Offline: {
return QObject::tr("");
}
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h
index c11aa146..895899a0 100644
--- a/launcher/minecraft/auth/AccountData.h
+++ b/launcher/minecraft/auth/AccountData.h
@@ -75,7 +75,8 @@ enum class AccountType {
MSA,
Mojang,
Offline,
- Elyby
+ Elyby,
+ Custom
};
enum class AccountState {
@@ -114,6 +115,8 @@ struct AccountData {
QString lastError() const;
+ QString customUrl const;
+
AccountType type = AccountType::MSA;
bool legacy = false;
bool canMigrateToMSA = false;
diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h
index a75df506..35107c4b 100644
--- a/launcher/minecraft/auth/AuthSession.h
+++ b/launcher/minecraft/auth/AuthSession.h
@@ -40,6 +40,8 @@ struct AuthSession
QString uuid;
// 'legacy' or 'mojang', depending on account type
QString user_type;
+ // Yggdrasil server url
+ QString url;
// Did the auth server reply?
bool auth_server_online = false;
// Did the user request online mode?
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index c8591fc3..85a387e4 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -52,6 +52,7 @@
#include "flows/Mojang.h"
#include "flows/Offline.h"
#include "flows/Elyby.h"
+#include "flows/Custom.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@@ -118,6 +119,17 @@ MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username)
return account;
}
+MinecraftAccountPtr MinecraftAccount::createCustom(const QString &username, const QString &url)
+{
+ MinecraftAccountPtr account = makeShared();
+
+ account->data.type = AccountType::Custom;
+ account->data.yggdrasilToken.extra["userName"] = username;
+ account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+ account->data.minecraftEntitlement.ownsMinecraft = true;
+ account->data.minecraftEntitlement.canPlayMinecraft = true;
+ return account;
+}
QJsonObject MinecraftAccount::saveToJson() const
{
@@ -185,6 +197,17 @@ shared_qobject_ptr MinecraftAccount::loginElyby(QString password) {
return m_currentTask;
}
+shared_qobject_ptr MinecraftAccount::loginCustom(QString password, QString url) {
+ Q_ASSERT(m_currentTask.get() == nullptr);
+
+ m_currentTask.reset(new customLogin(&data, password, url));
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
+ emit activityChanged(true);
+ return m_currentTask;
+}
+
shared_qobject_ptr MinecraftAccount::refresh() {
if(m_currentTask) {
return m_currentTask;
@@ -199,6 +222,9 @@ shared_qobject_ptr MinecraftAccount::refresh() {
else if(data.type == AccountType::Elyby) {
m_currentTask.reset(new ElybyRefresh(&data));
}
+ else if (data.type == AccountType::Custom) {
+ m_currentTask.reset(new CustomRefresh(&data));
+ }
else {
m_currentTask.reset(new MojangRefresh(&data));
}
@@ -328,6 +354,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
session->uuid = data.profileId();
// 'legacy' or 'mojang', depending on account type
session->user_type = typeString();
+ session->url = data.customUrl;
+
if (!session->access_token.isEmpty())
{
session->session = "token:" + data.accessToken() + ":" + data.profileId();
diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index 4985ab97..3bb50c5e 100644
--- a/launcher/minecraft/auth/MinecraftAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -97,6 +97,8 @@ public: /* construction */
static MinecraftAccountPtr createElyby(const QString &username);
+ static MinecraftAccountPtr createCustom(const QString &username, const QString &url);
+
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
@@ -117,6 +119,8 @@ public: /* manipulation */
shared_qobject_ptr loginElyby(QString password);
+ shared_qobject_ptr loginCustom(QString password, QString url);
+
shared_qobject_ptr refresh();
shared_qobject_ptr currentTask();
@@ -146,6 +150,10 @@ public: /* queries */
return data.profileName();
}
+ qString customUrl() const {
+ return data.customUrl();
+ }
+
bool isActive() const;
bool canMigrate() const {
@@ -168,6 +176,10 @@ public: /* queries */
return data.type == AccountType::Elyby;
}
+ bool isCustom() const {
+ return data.type == AccountType::Custom;
+ }
+
bool ownsMinecraft() const {
return data.minecraftEntitlement.ownsMinecraft;
}
@@ -192,10 +204,15 @@ public: /* queries */
case AccountType::Offline: {
return "offline";
}
+ break;
case AccountType::Elyby: {
return "elyby";
}
break;
+ case AccountType::Custom {
+ return "custom";
+ }
+ break;
default: {
return "unknown";
}
diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp
index acc026be..28e50145 100644
--- a/launcher/minecraft/auth/Yggdrasil.cpp
+++ b/launcher/minecraft/auth/Yggdrasil.cpp
@@ -129,6 +129,8 @@ void Yggdrasil::login(QString password, QString baseUrl) {
QJsonDocument doc(req);
+ this->customUrl = baseUrl;
+
QUrl reqUrl(baseUrl + "authenticate");
QNetworkRequest netRequest(reqUrl);
QByteArray requestData = doc.toJson();
diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h
index 34eb18b2..505917a6 100644
--- a/launcher/minecraft/auth/Yggdrasil.h
+++ b/launcher/minecraft/auth/Yggdrasil.h
@@ -97,6 +97,8 @@ protected:
QTimer counter;
int count = 0; // num msec since time reset
+ QString customUrl;
+
const int timeout_max = 30000;
const int time_step = 50;
};
diff --git a/launcher/minecraft/auth/flows/Custom.cpp b/launcher/minecraft/auth/flows/Custom.cpp
new file mode 100644
index 00000000..2b9b7654
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Custom.cpp
@@ -0,0 +1,25 @@
+#include "Custom.h"
+
+#include "minecraft/auth/steps/CustomStep.h"
+#include "minecraft/auth/steps/CustomProfileStep.h"
+#include "minecraft/auth/steps/GetSkinStep.h"
+
+customRefresh::customRefresh(
+ AccountData *data,
+ QObject *parent
+) : AuthFlow(data, parent) {
+ m_steps.append(makeShared(m_data, QString()));
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(m_data));
+}
+
+customLogin::customLogin(
+ AccountData *data,
+ QString password,
+ QString url,
+ QObject *parent
+): AuthFlow(data, parent), m_password(password), m_url(url) {
+ m_steps.append(makeShared(m_data, m_password, m_url));
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(m_data));
+}
diff --git a/launcher/minecraft/auth/flows/Custom.h b/launcher/minecraft/auth/flows/Custom.h
new file mode 100644
index 00000000..01c0f2f5
--- /dev/null
+++ b/launcher/minecraft/auth/flows/Custom.h
@@ -0,0 +1,27 @@
+#pragma once
+#include "AuthFlow.h"
+
+class customRefresh : public AuthFlow
+{
+ Q_OBJECT
+public:
+ explicit customRefresh(
+ AccountData *data,
+ QObject *parent = 0
+ );
+};
+
+class customLogin : public AuthFlow
+{
+ Q_OBJECT
+public:
+ explicit customLogin(
+ AccountData *data,
+ QString password,
+ QString url,
+ QObject *parent = 0
+ );
+
+private:
+ QString m_password;
+};
diff --git a/launcher/minecraft/auth/steps/CustomProfileStep.cpp b/launcher/minecraft/auth/steps/CustomProfileStep.cpp
new file mode 100644
index 00000000..496a4a7a
--- /dev/null
+++ b/launcher/minecraft/auth/steps/CustomProfileStep.cpp
@@ -0,0 +1,94 @@
+#include "CustomProfileStep.h"
+#include
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+#include "net/NetUtils.h"
+
+CustomProfileStep::CustomProfileStep(AccountData* data) : AuthStep(data) {
+
+}
+
+CustomProfileStep::~CustomProfileStep() noexcept = default;
+
+QString CustomProfileStep::describe() {
+ return tr("Fetching the Minecraft profile.");
+}
+
+
+void CustomProfileStep::perform() {
+ if (m_data->minecraftProfile.id.isEmpty()) {
+ emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
+ return;
+ }
+
+// m_data->
+
+ QUrl url = QUrl(m_data.customUrl + "/session/profile/" + m_data->minecraftProfile.id);
+ QNetworkRequest req = QNetworkRequest(url);
+ AuthRequest *request = new AuthRequest(this);
+ connect(request, &AuthRequest::finished, this, &CustomProfileStep::onRequestDone);
+ request->get(req);
+}
+
+void CustomProfileStep::rehydrate() {
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void CustomProfileStep::onRequestDone(
+ QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList headers
+) {
+ auto requestor = qobject_cast(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ if (error == QNetworkReply::ContentNotFoundError) {
+ // NOTE: Succeed even if we do not have a profile. This is a valid account state.
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(
+ AccountTaskState::STATE_SUCCEEDED,
+ tr("Account has no Minecraft profile.")
+ );
+ return;
+ }
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Error getting profile:";
+ qWarning() << " HTTP Status: " << requestor->httpStatus_;
+ qWarning() << " Internal error no.: " << error;
+ qWarning() << " Error string: " << requestor->errorString_;
+
+ qWarning() << " Response:";
+ qWarning() << QString::fromUtf8(data);
+
+ if (Net::isApplicationError(error)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
+ );
+ }
+ return;
+ }
+ if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile response could not be parsed")
+ );
+ return;
+ }
+
+ emit finished(
+ AccountTaskState::STATE_WORKING,
+ tr("Minecraft Java profile acquisition succeeded.")
+ );
+}
diff --git a/launcher/minecraft/auth/steps/CustomProfileStep.h b/launcher/minecraft/auth/steps/CustomProfileStep.h
new file mode 100644
index 00000000..725a829e
--- /dev/null
+++ b/launcher/minecraft/auth/steps/CustomProfileStep.h
@@ -0,0 +1,22 @@
+#pragma once
+#include
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+
+class CustomProfileStep : public AuthStep {
+ Q_OBJECT
+
+public:
+ explicit CustomProfileStep(AccountData *data);
+ virtual ~CustomProfileStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList);
+};
diff --git a/launcher/minecraft/auth/steps/CustomStep.cpp b/launcher/minecraft/auth/steps/CustomStep.cpp
new file mode 100644
index 00000000..54298ed9
--- /dev/null
+++ b/launcher/minecraft/auth/steps/CustomStep.cpp
@@ -0,0 +1,52 @@
+#include "CustomStep.h"
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+#include "minecraft/auth/Yggdrasil.h"
+
+CustomStep::CustomStep(AccountData* data, QString password, QString url) : AuthStep(data), m_password(password), m_url(url) {
+ m_yggdrasil = new Yggdrasil(m_data, this);
+
+ connect(m_yggdrasil, &Task::failed, this, &CustomStep::onAuthFailed);
+ connect(m_yggdrasil, &Task::succeeded, this, &CustomStep::onAuthSucceeded);
+ connect(m_yggdrasil, &Task::aborted, this, &CustomStep::onAuthFailed);
+}
+
+CustomStep::~CustomStep() noexcept = default;
+
+QString CustomStep::describe() {
+ return tr("Logging in with Custom account.");
+}
+
+void CustomStep::rehydrate() {
+ // NOOP, for now.
+}
+
+void CustomStep::perform() {
+ if(m_password.size()) {
+ m_yggdrasil->login(m_password, m_url + "/auth/");
+ }
+ else {
+ m_yggdrasil->refresh(m_url + "/auth/");
+ }
+}
+
+void CustomStep::onAuthSucceeded() {
+ emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Custom"));
+}
+
+void CustomStep::onAuthFailed() {
+ // TODO: hook these in again, expand to MSA
+ // m_error = m_yggdrasil->m_error;
+ // m_aborted = m_yggdrasil->m_aborted;
+
+ auto state = m_yggdrasil->taskState();
+ QString errorMessage = tr("Custom user authentication failed.");
+
+ // NOTE: soft error in the first step means 'offline'
+ if(state == AccountTaskState::STATE_FAILED_SOFT) {
+ state = AccountTaskState::STATE_OFFLINE;
+ errorMessage = tr("Custom user authentication ended with a network error. Is MutliFactor Auth current?");
+ }
+ emit finished(state, errorMessage);
+}
diff --git a/launcher/minecraft/auth/steps/CustomStep.h b/launcher/minecraft/auth/steps/CustomStep.h
new file mode 100644
index 00000000..bff05e44
--- /dev/null
+++ b/launcher/minecraft/auth/steps/CustomStep.h
@@ -0,0 +1,28 @@
+#pragma once
+#include
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class Yggdrasil;
+
+class CustomStep : public AuthStep {
+ Q_OBJECT
+
+public:
+ explicit CustomStep(AccountData *data, QString password, QString url);
+ virtual ~CustomStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+private slots:
+ void onAuthSucceeded();
+ void onAuthFailed();
+
+private:
+ Yggdrasil *m_yggdrasil = nullptr;
+ QString m_password;
+};
diff --git a/launcher/minecraft/launch/InjectAuthlib.cpp b/launcher/minecraft/launch/InjectAuthlib.cpp
index 06f31739..eef9f605 100644
--- a/launcher/minecraft/launch/InjectAuthlib.cpp
+++ b/launcher/minecraft/launch/InjectAuthlib.cpp
@@ -20,9 +20,10 @@
#include
#include
-InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent)
+InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector, QString url) : LaunchStep(parent)
{
m_injector = injector;
+ m_url = url;
}
void InjectAuthlib::executeTask()
@@ -130,7 +131,7 @@ void InjectAuthlib::onVersionDownloadSucceeded()
void InjectAuthlib::onDownloadSucceeded()
{
- QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg("ely.by");
+ QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg(m_url);
qDebug()
<< "Injecting " << injector;
diff --git a/launcher/minecraft/launch/InjectAuthlib.h b/launcher/minecraft/launch/InjectAuthlib.h
index 5274f55d..260b00df 100644
--- a/launcher/minecraft/launch/InjectAuthlib.h
+++ b/launcher/minecraft/launch/InjectAuthlib.h
@@ -71,5 +71,6 @@ private:
bool m_offlineMode;
QString m_versionName;
QString m_authServer;
+ QString m_url;
AuthlibInjectorPtr *m_injector;
};
diff --git a/launcher/ui/dialogs/CustomLoginDialog.cpp b/launcher/ui/dialogs/CustomLoginDialog.cpp
new file mode 100644
index 00000000..2789143b
--- /dev/null
+++ b/launcher/ui/dialogs/CustomLoginDialog.cpp
@@ -0,0 +1,131 @@
+/* Copyright 2013-2021 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 "CustomLoginDialog.h"
+#include "ui_CustomLoginDialog.h"
+
+#include "minecraft/auth/AccountTask.h"
+
+#include
+
+CustomLoginDialog::CustomLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CustomLoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+CustomLoginDialog::~CustomLoginDialog()
+{
+ delete ui;
+}
+
+// Stage 1: User interaction
+void CustomLoginDialog::accept()
+{
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createCustom(ui->userTextBox->text());
+ if (ui->mfaTextBox->text().length() > 0) {
+ m_loginTask = m_account->loginCustom(ui->passTextBox->text() + ':' + ui->mfaTextBox->text(), ui->urlTextBox->text());
+ }
+ else {
+ m_loginTask = m_account->loginCustom(ui->passTextBox->text(), ui->urlTextBox->text());
+ }
+ connect(m_loginTask.get(), &Task::failed, this, &CustomLoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this, &CustomLoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this, &CustomLoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this, &CustomLoginDialog::onTaskProgress);
+ m_loginTask->start();
+}
+
+void CustomLoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->userTextBox->setEnabled(enable);
+ ui->passTextBox->setEnabled(enable);
+ ui->urlTextBox->setEnabled(enable);
+ ui->mfaTextBox->setEnabled(enable);
+ ui->buttonBox->setEnabled(enable);
+}
+
+// Enable the OK button only when both textboxes contain something.
+void CustomLoginDialog::on_userTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty() && !ui->urlTextBox->text().isEmpty());
+}
+void CustomLoginDialog::on_passTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty() && !ui->urlTextBox->text().isEmpty());
+}
+void CustomLoginDialog::on_urlTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty() && !ui->passTextBox->text().isEmpty());
+}
+
+void CustomLoginDialog::onTaskFailed(const QString &reason)
+{
+ // Set message
+ auto lines = reason.split('\n');
+ QString processed;
+ for(auto line: lines) {
+ if(line.size()) {
+ processed += "" + line + "
";
+ }
+ else {
+ processed += "
";
+ }
+ }
+ ui->label->setText(processed);
+
+ // Re-enable user-interaction
+ setUserInputsEnabled(true);
+ ui->progressBar->setVisible(false);
+}
+
+void CustomLoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void CustomLoginDialog::onTaskStatus(const QString &status)
+{
+ ui->label->setText(status);
+}
+
+void CustomLoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr CustomLoginDialog::newAccount(QWidget *parent, QString msg)
+{
+ CustomLoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return nullptr;
+}
diff --git a/launcher/ui/dialogs/CustomLoginDialog.h b/launcher/ui/dialogs/CustomLoginDialog.h
new file mode 100644
index 00000000..4be92c11
--- /dev/null
+++ b/launcher/ui/dialogs/CustomLoginDialog.h
@@ -0,0 +1,60 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "minecraft/auth/MinecraftAccount.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class CustomLoginDialog;
+}
+
+class CustomLoginDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ~CustomLoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
+
+private:
+ explicit CustomLoginDialog(QWidget *parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+protected
+slots:
+ void accept();
+
+ void onTaskFailed(const QString &reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString &status);
+ void onTaskProgress(qint64 current, qint64 total);
+
+ void on_userTextBox_textEdited(const QString &newText);
+ void on_passTextBox_textEdited(const QString &newText);
+ void on_urlTextBox_textEdited(const QString &newText);
+
+private:
+ Ui::CustomLoginDialog *ui;
+ MinecraftAccountPtr m_account;
+ Task::Ptr m_loginTask;
+};
diff --git a/launcher/ui/dialogs/CustomLoginDialog.ui b/launcher/ui/dialogs/CustomLoginDialog.ui
new file mode 100644
index 00000000..5a51e9da
--- /dev/null
+++ b/launcher/ui/dialogs/CustomLoginDialog.ui
@@ -0,0 +1,94 @@
+
+
+ CustomLoginDialog
+
+
+
+ 0
+ 0
+ 421
+ 198
+
+
+
+
+ 0
+ 0
+
+
+
+ Add Account
+
+
+ -
+
+
+ Message label placeholder.
+
+
+ Qt::RichText
+
+
+ Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+ -
+
+
+ Email
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+ Password
+
+
+
+ -
+
+
+ Url
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+ 2FA Code (Optional)
+
+
+
+ -
+
+
+ 24
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index 134406f6..e3587dab 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -49,6 +49,7 @@
#include "ui/dialogs/LoginDialog.h"
#include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/ElybyLoginDialog.h"
+#include "ui/dialogs/CustomLoginDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/SkinUploadDialog.h"
@@ -219,6 +220,22 @@ void AccountListPage::on_actionAddElyby_triggered()
}
}
+void AccountListPage::on_actionAddCustom_triggered()
+{
+ MinecraftAccountPtr account = CustomLoginDialog::newAccount(
+ this,
+ tr("Enter the custom yggdrasil server url, along with your username and password to add your account.")
+ );
+
+ if (account)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1) {
+ m_account->setDefaultAccount(account);
+ }
+ }
+}
+
void AccountListPage::on_actionRemove_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
@@ -263,6 +280,7 @@ void AccountListPage::updateButtonStates()
bool accountIsReady = false;
bool accountIsOnline = false;
bool accountIsElyby = false;
+ bool AccountIsCustom = false;
if (hasSelection)
{
QModelIndex selected = selection.first();
@@ -270,12 +288,13 @@ void AccountListPage::updateButtonStates()
accountIsReady = !account->isActive();
accountIsOnline = !account->isOffline();
accountIsElyby = account->isElyby();
+ accountIsCustom = account->isCustom();
}
ui->actionRemove->setEnabled(accountIsReady);
ui->actionSetDefault->setEnabled(accountIsReady);
- ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby);
- ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby);
+ ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby && !accountIsCustom);
+ ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby && !accountIsCustom);
ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline);
if(m_accounts->defaultAccount().get() == nullptr) {
diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h
index 1812c5e5..edd5b257 100644
--- a/launcher/ui/pages/global/AccountListPage.h
+++ b/launcher/ui/pages/global/AccountListPage.h
@@ -86,6 +86,7 @@ public slots:
void on_actionAddMicrosoft_triggered();
void on_actionAddOffline_triggered();
void on_actionAddElyby_triggered();
+ void on_actionAddCustom_triggered();
void on_actionRemove_triggered();
void on_actionRefresh_triggered();
void on_actionSetDefault_triggered();
diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui
index e5d76ec4..dd53c8c0 100644
--- a/launcher/ui/pages/global/AccountListPage.ui
+++ b/launcher/ui/pages/global/AccountListPage.ui
@@ -56,6 +56,7 @@
+
@@ -115,6 +116,11 @@
Add &Ely.by
+
+
+ Add &Custom
+
+
&Refresh