diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index d6b8694279..4f9e73506f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -231,6 +231,8 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/Mojang.h minecraft/auth/flows/MSA.cpp minecraft/auth/flows/MSA.h + minecraft/auth/flows/Local.cpp + minecraft/auth/flows/Local.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h @@ -254,6 +256,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/XboxUserStep.h minecraft/auth/steps/YggdrasilStep.cpp minecraft/auth/steps/YggdrasilStep.h + minecraft/auth/steps/LocalStep.cpp + minecraft/auth/steps/LocalStep.h minecraft/gameoptions/GameOptions.h minecraft/gameoptions/GameOptions.cpp diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 053774d579..e3921c48a7 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -121,13 +121,89 @@ void LaunchController::login() { m_session->wants_online = m_online; m_accountToUse->fillSession(m_session); + if (m_accountToUse->accountType() == AccountType::Local) { + launchInstance(); + return; + } + switch(m_accountToUse->accountState()) { case AccountState::Offline: { m_session->wants_online = false; // NOTE: fallthrough is intentional } case AccountState::Online: { - launchInstance(); + if(!m_session->wants_online) { + QString usedname; + if(m_offlineName.isEmpty()) { + // we ask the user for a player name + bool ok = false; + QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName; + QString name = QInputDialog::getText( + m_parentWidget, + tr("Player name"), + tr("Choose your offline mode player name."), + QLineEdit::Normal, + usedname, + &ok + ); + if (!ok) + { + tryagain = false; + break; + } + if (name.length()) + { + usedname = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedname); + } + } + else { + usedname = m_offlineName; + } + + m_session->MakeOffline(usedname); + // offline flavored game from here :3 + } + if(m_accountToUse->ownsMinecraft()) { + if(!m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() == QDialog::Accepted) + { + tryagain = true; + continue; + } + else + { + emitFailed(tr("Received undetermined session status during login.")); + return; + } + } + // we own Minecraft, there is a profile, it's all ready to go! + launchInstance(); + return; + } + else { + // play demo ? + QMessageBox box(m_parentWidget); + box.setWindowTitle(tr("Play demo?")); + box.setText(tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play the demo?")); + box.setIcon(QMessageBox::Warning); + auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + box.setDefaultButton(cancelButton); + + box.exec(); + if(box.clickedButton() == demoButton) { + // play demo here + m_session->MakeDemo(); + launchInstance(); + } + else { + emitFailed(tr("Launch cancelled - account does not own Minecraft.")); + } + } return; } case AccountState::Errored: diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index d52bfe773b..6770b85860 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -87,3 +87,10 @@ class shared_qobject_ptr private: std::shared_ptr m_ptr; }; + +template +shared_qobject_ptr makeShared(Args... args) +{ + auto obj = new T(args...); + return shared_qobject_ptr(obj); +} diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 0a7281fb2c..cd0813f22a 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -917,19 +917,13 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt } // if we aren't in offline mode,. - /*if(session->status != AuthSession::PlayableOffline) - { - process->appendStep(new ClaimAccount(pptr, session)); - }*/ - - // do update only if we're in online mode - if (session->wants_online) - { - process->appendStep(new Update(pptr, Net::Mode::Online)); - } - else - { - process->appendStep(new Update(pptr, Net::Mode::Offline)); + if (session->status != AuthSession::PlayableOffline) { + if (!session->demo) { + process->appendStep(makeShared(pptr, session)); + } + process->appendStep(makeShared(pptr, Net::Mode::Online)); + } else { + process->appendStep(makeShared(pptr, Net::Mode::Offline)); } // if there are any jar mods diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 6c2f41685f..aef63df81b 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -314,8 +314,11 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { if(typeS == "MSA") { type = AccountType::MSA; provider = AuthProviders::lookup("MSA"); - } else { + } else if (typeS == "Mojang") { type = AccountType::Mojang; + provider = AuthProviders::lookup("Mojang"); + } else { + type = AccountType::Local; provider = AuthProviders::lookup(typeS); } @@ -349,7 +352,7 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { QJsonObject AccountData::saveState() const { QJsonObject output; if(type == AccountType::Mojang) { - output["type"] = provider->id(); + output["type"] = "Mojang"; if(legacy) { output["legacy"] = true; } @@ -366,6 +369,8 @@ QJsonObject AccountData::saveState() const { tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); + } else if (type == AccountType::Local) { + output["type"] = "Local"; } tokenToJSONV3(output, yggdrasilToken, "ygg"); @@ -386,14 +391,14 @@ QString AccountData::accessToken() const { } QString AccountData::clientToken() const { - if(type != AccountType::Mojang) { + if(type == AccountType::MSA) { return QString(); } return yggdrasilToken.extra["clientToken"].toString(); } void AccountData::setClientToken(QString clientToken) { - if(type != AccountType::Mojang) { + if(type == AccountType::MSA) { return; } yggdrasilToken.extra["clientToken"] = clientToken; @@ -407,7 +412,7 @@ void AccountData::generateClientTokenIfMissing() { } void AccountData::invalidateClientToken() { - if(type != AccountType::Mojang) { + if(type == AccountType::MSA) { return; } yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]")); @@ -432,6 +437,9 @@ QString AccountData::accountDisplayString() const { case AccountType::Mojang: { return userName(); } + case AccountType::Local: { + return ""; + } case AccountType::MSA: { if(xboxApiToken.extra.contains("gtg")) { return xboxApiToken.extra["gtg"].toString(); diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 837d637a13..8fbdde451e 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -40,7 +40,8 @@ struct MinecraftProfile { enum class AccountType { MSA, - Mojang + Mojang, + Local }; enum class AccountState { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 380bf876ad..0bceaa12a8 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -17,6 +17,7 @@ #include "MinecraftAccount.h" +#include #include #include #include @@ -31,11 +32,13 @@ #include "AuthProviders.h" #include "flows/MSA.h" #include "flows/Mojang.h" +#include "flows/Local.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); } + MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); if(account->data.resumeStateFromV2(json)) { @@ -58,7 +61,23 @@ MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username account->data.type = AccountType::Mojang; account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); - account->data.minecraftProfile.id = account->data.internalId; + return account; +} + +MinecraftAccountPtr MinecraftAccount::createLocal(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Local; + account->data.yggdrasilToken.token = "0"; + account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; + account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.validity = Katabasis::Validity::Certain; return account; } @@ -113,16 +132,27 @@ shared_qobject_ptr MinecraftAccount::loginMSA() { return m_currentTask; } +shared_qobject_ptr MinecraftAccount::loginLocal() { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new LocalLogin(&data)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + shared_qobject_ptr MinecraftAccount::refresh() { if(m_currentTask) { return m_currentTask; } - if(data.type == AccountType::MSA) { + if (data.type == AccountType::MSA) { m_currentTask.reset(new MSASilent(&data)); - } - else { + } else if (data.type == AccountType::Mojang) { m_currentTask.reset(new MojangRefresh(&data)); + } else { + m_currentTask.reset(new LocalRefresh(&data)); } connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); @@ -279,3 +309,32 @@ void MinecraftAccount::incrementUses() qWarning() << "Profile" << data.profileId() << "is now in use."; } } + +QUuid MinecraftAccount::uuidFromUsername(QString username) { + auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); + + // basically a reimplementation of Java's UUID#nameUUIDFromBytes + QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto bOr = [](QByteArray& array, int index, char value) { + array[index] = array.at(index) | value; + }; + auto bAnd = [](QByteArray& array, int index, char value) { + array[index] = array.at(index) & value; + }; +#else + auto bOr = [](QByteArray& array, qsizetype index, char value) { + array[index] |= value; + }; + auto bAnd = [](QByteArray& array, qsizetype index, char value) { + array[index] &= value; + }; +#endif + bAnd(digest, 6, (char) 0x0f); // clear version + bOr(digest, 6, (char) 0x30); // set to version 3 + bAnd(digest, 8, (char) 0x3f); // clear variant + bOr(digest, 8, (char) 0x80); // set to IETF variant + + return QUuid::fromRfc4122(digest); +} diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 7f79b6a0d6..cd99a0a919 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -74,11 +74,15 @@ class MinecraftAccount : static MinecraftAccountPtr createFromUsername(const QString &username); + static MinecraftAccountPtr createLocal(const QString &username); + static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); + static QUuid uuidFromUsername(QString username); + //! Saves a MinecraftAccount to a JSON object and returns it. QJsonObject saveToJson() const; @@ -92,6 +96,8 @@ class MinecraftAccount : shared_qobject_ptr loginMSA(); + shared_qobject_ptr loginLocal(); + shared_qobject_ptr refresh(); shared_qobject_ptr currentTask(); @@ -161,6 +167,10 @@ class MinecraftAccount : return "msa"; } break; + case AccountType::Local: { + return "local"; + } + break; default: { return "unknown"; } diff --git a/launcher/minecraft/auth/flows/Local.cpp b/launcher/minecraft/auth/flows/Local.cpp new file mode 100644 index 0000000000..63a2681c17 --- /dev/null +++ b/launcher/minecraft/auth/flows/Local.cpp @@ -0,0 +1,13 @@ +#include "Local.h" + +#include "minecraft/auth/steps/LocalStep.h" + +LocalRefresh::LocalRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent) +{ + m_steps.append(makeShared(m_data)); +} + +LocalLogin::LocalLogin(AccountData* data, QObject* parent) : AuthFlow(data, parent) +{ + m_steps.append(makeShared(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Local.h b/launcher/minecraft/auth/flows/Local.h new file mode 100644 index 0000000000..781c9e97bc --- /dev/null +++ b/launcher/minecraft/auth/flows/Local.h @@ -0,0 +1,14 @@ +#pragma once +#include "AuthFlow.h" + +class LocalRefresh : public AuthFlow { + Q_OBJECT + public: + explicit LocalRefresh(AccountData* data, QObject* parent = 0); +}; + +class LocalLogin : public AuthFlow { + Q_OBJECT + public: + explicit LocalLogin(AccountData* data, QObject* parent = 0); +}; diff --git a/launcher/minecraft/auth/steps/LocalStep.cpp b/launcher/minecraft/auth/steps/LocalStep.cpp new file mode 100644 index 0000000000..8aa230e24a --- /dev/null +++ b/launcher/minecraft/auth/steps/LocalStep.cpp @@ -0,0 +1,21 @@ +#include "LocalStep.h" + +#include "Application.h" + +LocalStep::LocalStep(AccountData* data) : AuthStep(data) {} +LocalStep::~LocalStep() noexcept = default; + +QString LocalStep::describe() +{ + return tr("Creating local account."); +} + +void LocalStep::rehydrate() +{ + // NOOP +} + +void LocalStep::perform() +{ + emit finished(AccountTaskState::STATE_WORKING, tr("Created local account.")); +} diff --git a/launcher/minecraft/auth/steps/LocalStep.h b/launcher/minecraft/auth/steps/LocalStep.h new file mode 100644 index 0000000000..bc24260ba8 --- /dev/null +++ b/launcher/minecraft/auth/steps/LocalStep.h @@ -0,0 +1,19 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include + +class LocalStep : public AuthStep { + Q_OBJECT + public: + explicit LocalStep(AccountData* data); + virtual ~LocalStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; +}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index 7cc35360d0..add9165998 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -42,7 +42,6 @@ void MinecraftProfileStep::onRequestDone( #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. if(m_data->type == AccountType::Mojang) { @@ -70,23 +69,21 @@ void MinecraftProfileStep::onRequestDone( tr("Minecraft Java profile acquisition failed.") ); return; - }*/ - /*if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { + } + if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed") ); return; - }*/ + } if(m_data->type == AccountType::Mojang) { - //auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; - // IDK if this should be modified or not but still just to be sure - m_data->minecraftEntitlement.canPlayMinecraft = true; - m_data->minecraftEntitlement.ownsMinecraft = true; + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; } - emit finished( AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded.") diff --git a/launcher/ui/dialogs/LocalLoginDialog.cpp b/launcher/ui/dialogs/LocalLoginDialog.cpp index 230094bc53..79b7977791 100644 --- a/launcher/ui/dialogs/LocalLoginDialog.cpp +++ b/launcher/ui/dialogs/LocalLoginDialog.cpp @@ -27,22 +27,16 @@ void LocalLoginDialog::accept() setUserInputsEnabled(false); ui->progressBar->setVisible(true); - m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); + m_account = MinecraftAccount::createLocal(ui->userTextBox->text()); m_account->setProvider(AuthProviders::lookup("dummy")); // Setup the login task and start it - const char *dummy_password = " "; - m_loginTask = m_account->login(QString::fromLatin1(dummy_password)); + m_loginTask = m_account->loginLocal(); connect(m_loginTask.get(), &Task::failed, this, &LocalLoginDialog::onTaskFailed); connect(m_loginTask.get(), &Task::succeeded, this, &LocalLoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &LocalLoginDialog::onTaskStatus); connect(m_loginTask.get(), &Task::progress, this, &LocalLoginDialog::onTaskProgress); - if (!m_loginTask) - { - onTaskSucceeded(); - } else { - m_loginTask->start(); - } + m_loginTask->start(); } void LocalLoginDialog::setUserInputsEnabled(bool enable) diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 7449d34262..76b6af4981 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -59,9 +59,7 @@ ProfileSetupDialog::~ProfileSetupDialog() void ProfileSetupDialog::on_buttonBox_accepted() { - //setNameStatus(NameStatus::Available); - accept(); - //setupProfile(currentCheck); + setupProfile(currentCheck); } void ProfileSetupDialog::on_buttonBox_rejected() @@ -173,8 +171,7 @@ void ProfileSetupDialog::checkFinished( } } else { - setNameStatus(NameStatus::Available); - //setNameStatus(NameStatus::Error, tr("Failed to check name availability.")); + setNameStatus(NameStatus::Error, tr("Failed to check name availability.")); } isChecking = false; } @@ -241,7 +238,7 @@ void ProfileSetupDialog::setupProfileFinished( requestor->deleteLater(); isWorking = false; - if(error != QNetworkReply::NoError) { + if(error == QNetworkReply::NoError) { /* * data contains the profile in the response * ... we could parse it and update the account, but let's just return back to the normal login flow instead... diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 102b6fa46f..4aa9620f18 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -215,24 +215,19 @@ void AccountListPage::updateButtonStates() QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); bool hasSelection = selection.size() > 0; bool accountIsReady = false; + bool accountIsOnline = false; if (hasSelection) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); accountIsReady = !account->isActive(); + accountIsOnline = account->accountType() != AccountType::Local; } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - // Don't enable skin change buttons for dummy and ely.by accounts, they don't work. - if (hasSelection) { - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - if (account->provider()->id() != "dummy" && account->provider()->id() != "elyby") { - ui->actionUploadSkin->setEnabled(accountIsReady); - ui->actionDeleteSkin->setEnabled(accountIsReady); - } - } - ui->actionRefresh->setEnabled(accountIsReady); + ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if(m_accounts->defaultAccount().get() == nullptr) { ui->actionNoDefault->setEnabled(false);