From 1b68d51da634ddab39fe872fcc28a4f491c0c8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 20 Aug 2021 01:34:32 +0200 Subject: [PATCH] NOISSUE add setting capes, tweak missing profile message, fix cape IDs --- launcher/CMakeLists.txt | 2 + launcher/dialogs/LoginDialog.cpp | 12 +- launcher/dialogs/MSALoginDialog.cpp | 12 +- launcher/dialogs/MSALoginDialog.ui | 10 +- launcher/dialogs/SkinUploadDialog.cpp | 44 ++++- launcher/dialogs/SkinUploadDialog.ui | 170 ++++++++++-------- launcher/minecraft/auth/AccountData.cpp | 76 ++++---- launcher/minecraft/auth/AccountData.h | 4 +- launcher/minecraft/auth/flows/AuthContext.cpp | 18 +- launcher/minecraft/services/CapeChange.cpp | 67 +++++++ launcher/minecraft/services/CapeChange.h | 32 ++++ 11 files changed, 317 insertions(+), 130 deletions(-) create mode 100644 launcher/minecraft/services/CapeChange.cpp create mode 100644 launcher/minecraft/services/CapeChange.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3c140ede..84a03895 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -329,6 +329,8 @@ set(MINECRAFT_SOURCES minecraft/AssetsUtils.cpp # Minecraft services + minecraft/services/CapeChange.cpp + minecraft/services/CapeChange.h minecraft/services/SkinUpload.cpp minecraft/services/SkinUpload.h minecraft/services/SkinDelete.cpp diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp index 1dee9920..b1ca2c88 100644 --- a/launcher/dialogs/LoginDialog.cpp +++ b/launcher/dialogs/LoginDialog.cpp @@ -73,7 +73,17 @@ void LoginDialog::on_passTextBox_textEdited(const QString &newText) void LoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("" + reason + ""); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "\n"; + } + else { + processed += '\n'; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 778b379d..86ebdf91 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -60,7 +60,17 @@ void MSALoginDialog::setUserInputsEnabled(bool enable) void MSALoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("" + reason + ""); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "\n"; + } + else { + processed += '\n'; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui index 4ae8085a..5479a726 100644 --- a/launcher/dialogs/MSALoginDialog.ui +++ b/launcher/dialogs/MSALoginDialog.ui @@ -6,8 +6,8 @@ 0 0 - 421 - 114 + 491 + 143 @@ -23,10 +23,12 @@ - Message label placeholder. + Message label placeholder. + +aaaaa - Qt::RichText + Qt::MarkdownText Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp index 19bfac4d..97478f4b 100644 --- a/launcher/dialogs/SkinUploadDialog.cpp +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -1,11 +1,16 @@ #include #include +#include + #include #include +#include + #include "SkinUploadDialog.h" #include "ui_SkinUploadDialog.h" #include "ProgressDialog.h" #include "CustomMessageBox.h" +#include void SkinUploadDialog::on_buttonBox_rejected() { @@ -85,8 +90,13 @@ void SkinUploadDialog::on_buttonBox_accepted() { model = SkinUpload::ALEX; } - SkinUploadPtr upload = std::make_shared(this, session, FS::read(fileName), model); - if (prog.execWithTask((Task*)upload.get()) != QDialog::Accepted) + SequentialTask skinUpload; + skinUpload.addTask(std::make_shared(this, session, FS::read(fileName), model)); + auto selectedCape = ui->capeCombo->currentData().toString(); + if(selectedCape != session->m_accountPtr->accountData()->minecraftProfile.currentCape) { + skinUpload.addTask(std::make_shared(this, session, selectedCape)); + } + if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); close(); @@ -111,4 +121,34 @@ SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent) :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) { ui->setupUi(this); + + // FIXME: add a model for this, download/refresh the capes on demand + auto &data = *acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = data.minecraftProfile.currentCape; + if(currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + for(auto & cape: data.minecraftProfile.capes) { + index++; + if(cape.data.size()) { + QPixmap capeImage; + if(capeImage.loadFromData(cape.data, "PNG")) { + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + continue; + } + } + ui->capeCombo->addItem(cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + } } diff --git a/launcher/dialogs/SkinUploadDialog.ui b/launcher/dialogs/SkinUploadDialog.ui index 6f5307e3..f4b0ed0a 100644 --- a/launcher/dialogs/SkinUploadDialog.ui +++ b/launcher/dialogs/SkinUploadDialog.ui @@ -1,85 +1,97 @@ - SkinUploadDialog - - - - 0 - 0 - 413 - 300 - + SkinUploadDialog + + + + 0 + 0 + 394 + 360 + + + + Skin Upload + + + + + + Skin File + + + + + + + + + + 0 + 0 + - - Skin Upload + + + 28 + 16777215 + - - - - - Skin File - - - - - - - - - - 0 - 0 - - - - - 28 - 16777215 - - - - ... - - - - - - - - - - Player Model - - - - - - Steve Model - - - true - - - - - - - Alex Model - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + + ... + + + + - - + + + + + Player Model + + + + + + Steve Model + + + true + + + + + + + Alex Model + + + + + + + + + + Cape + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 77c73c1b..5c6de9df 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -78,8 +78,8 @@ void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * token QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); - if(p.currentCape != -1) { - out["cape"] = p.capes[p.currentCape].id; + if(!p.currentCape.isEmpty()) { + out["cape"] = p.currentCape; } { @@ -155,41 +155,53 @@ MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * token } } - auto capesV = tokenObject.value("capes"); - if(!capesV.isArray()) { - qWarning() << "capes is not an array!"; - return MinecraftProfile(); - } - auto capesArray = capesV.toArray(); - for(auto capeV: capesArray) { - if(!capeV.isObject()) { - qWarning() << "cape is not an object!"; + { + auto capesV = tokenObject.value("capes"); + if(!capesV.isArray()) { + qWarning() << "capes is not an array!"; return MinecraftProfile(); } - auto capeObj = capeV.toObject(); - auto idV = capeObj.value("id"); - auto urlV = capeObj.value("url"); - auto aliasV = capeObj.value("alias"); - if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { - qWarning() << "mandatory skin attributes are missing or of unexpected type"; - return MinecraftProfile(); - } - Cape cape; - cape.id = idV.toString(); - cape.url = urlV.toString(); - cape.alias = aliasV.toString(); + auto capesArray = capesV.toArray(); + for(auto capeV: capesArray) { + if(!capeV.isObject()) { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.alias = aliasV.toString(); - // data for cape is optional. - auto dataV = capeObj.value("data"); - if(dataV.isString()) { - // TODO: validate base64 - cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + // data for cape is optional. + auto dataV = capeObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes[cape.id] = cape; } - else if (!dataV.isUndefined()) { - qWarning() << "cape data is something unexpected"; - return MinecraftProfile(); + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if(capeV.isString()) { + auto currentCape = capeV.toString(); + if(out.capes.contains(currentCape)) { + out.currentCape = currentCape; + } } - out.capes.push_back(cape); } out.validity = Katabasis::Validity::Assumed; return out; diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index b2d09cb0..cf58fb76 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -25,8 +25,8 @@ struct MinecraftProfile { QString id; QString name; Skin skin; - int currentCape = -1; - QVector capes; + QString currentCape; + QMap capes; Katabasis::Validity validity = Katabasis::Validity::None; }; diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index d6a72208..9754d1a9 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -576,7 +576,9 @@ void AuthContext::onXBoxProfileDone( } void AuthContext::checkResult() { + qDebug() << "AuthContext::checkResult called"; if(m_requestsDone != 2) { + qDebug() << "Number of ready results:" << m_requestsDone; return; } if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { @@ -638,10 +640,9 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { break; } auto capesArray = obj.value("capes").toArray(); - int i = -1; - int currentCape = -1; + + QString currentCape; for(auto cape: capesArray) { - i++; auto capeObj = cape.toObject(); Cape capeOut; if(!getString(capeObj.value("id"), capeOut.id)) { @@ -652,7 +653,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { continue; } if(state == "ACTIVE") { - currentCape = i; + currentCape = capeOut.id; } if(!getString(capeObj.value("url"), capeOut.url)) { continue; @@ -661,8 +662,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { continue; } - // we deal with only the active skin - output.capes.push_back(capeOut); + output.capes[capeOut.id] = capeOut; } output.currentCape = currentCape; output.validity = Katabasis::Validity::Certain; @@ -692,18 +692,18 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, if (error == QNetworkReply::ContentNotFoundError) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, tr("Account is missing a profile")); + changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); return; } if (error != QNetworkReply::NoError) { finishActivity(); - changeState(STATE_FAILED_HARD, tr("Profile acquisition failed")); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); return; } if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, tr("Profile response could not be parsed")); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); return; } doGetSkin(); diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp new file mode 100644 index 00000000..c1d88d14 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.cpp @@ -0,0 +1,67 @@ +#include "CapeChange.h" +#include +#include +#include + +CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) + : Task(parent), m_capeId(cape), m_session(session) +{ +} + +void CapeChange::setCape(QString& cape) { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().put(request, requestString.toUtf8()); + + setStatus(tr("Equipping cape")); + + m_reply = std::shared_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void CapeChange::clearCape() { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + + setStatus(tr("Removing cape")); + + m_reply = std::shared_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void CapeChange::executeTask() +{ + if(m_capeId.isEmpty()) { + clearCape(); + } + else { + setCape(m_capeId); + } +} + +void CapeChange::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void CapeChange::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h new file mode 100644 index 00000000..1b6f2f72 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include "tasks/Task.h" + +class CapeChange : public Task +{ + Q_OBJECT +public: + CapeChange(QObject *parent, AuthSessionPtr session, QString capeId); + virtual ~CapeChange() {} + +private: + void setCape(QString & cape); + void clearCape(); + +private: + QString m_capeId; + AuthSessionPtr m_session; + std::shared_ptr m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; +