diff options
116 files changed, 2742 insertions, 1494 deletions
@@ -21,6 +21,10 @@ Debug build /build-* +# Install dirs +install +/install-* + # Ctags File tags @@ -102,7 +102,7 @@ Getting the project to build and run on Windows is easy if you use Qt's IDE, Qt - Microsoft Visual C++ 2008 Redist is required for this, there's a link on the OpenSSL download page above next to the main download. - We use a custom build of OpenSSL that doesn't have this dependency. For normal development, the custom build is not necessary though. * [zlib 1.2+](http://gnuwin32.sourceforge.net/packages/zlib.htm) - the Setup is fine -* [Java JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* [Java JDK 8](https://adoptium.net/releases.html?variant=openjdk8) - Use the MSI installer. * [CMake](http://www.cmake.org/cmake/resources/software.html) -- Windows (Win32 Installer) Ensure that OpenSSL, zlib, Java and CMake are on `PATH`. diff --git a/CMakeLists.txt b/CMakeLists.txt index e45dbf7c..a0a8c5ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ set(Launcher_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fet ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 0) set(Launcher_VERSION_MINOR 6) -set(Launcher_VERSION_HOTFIX 13) +set(Launcher_VERSION_HOTFIX 14) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") @@ -32,7 +32,7 @@ In general, in order of importance: Translations can be done [on crowdin](https://translate.multimc.org). Please avoid making direct pull requests to the translations repository. ## Forking/Redistributing/Custom builds policy -We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license. +We keep Launcher open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license. Part of the reason for using the Apache license is that we don't want people using the "MultiMC" name when redistributing the project. This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title). diff --git a/changelog.md b/changelog.md index b2cbdd81..7b1d4ae8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,69 @@ -# MultiMC 0.6.13 +# MultiMC 0.6.14 + +This further refines Microsoft account support, along with small fixes related to modpack platforms and Java runtime detection. + +It's also been 10 years since the first release of MultiMC. All background cats are now ready to party! + +### Microsoft accounts + +The account system now refreshes accounts in the background while the application is running. + +- GH-4071: Errors encountered while refreshing account tokens no longer always result in the tokens expiring: + - Network errors encountered when refreshing the main account tokens result in the account being **Offline**. + - **Hard** errors are produced by the main tokens becoming provably invalid. + - Errors encountered later are treated as **Soft** - they do make the account unusable, but still recoverable by trying again. + - **Soft** errors are treated as **Hard** errors when adding the account initially. + +In general, this should make MultiMC much more forgiving towards various temporary and non-fatal errors. + +- GH-4217: Added support for GamePass accounts and Minecraft profile setup: + - The new endpoint for logging in with Microsoft is now used (`/launcher/login`), enabling compatibility with GamePass. + - Game ownership is checked instead of only relying on Minecraft profile presence. + - Accounts can now be added even when they do not have a profile. + - The launcher should guide you through selecting a Minecraft name if you don't have one yet. + +### Modpack platform changes + +- GH-4055: MultiMC now tries to avoid downloading multiple files to the same path for FTB modpacks. + +- Search as you type is now used for FTB. + +- GH-4185: Version of the modpack is now included in the name of the instance by default. + +- The modpack platform UIs now include text field clear buttons. + +### Other changes + +- Adjusted warnings about Java runtime required for Minecraft 1.18 (it's not Java 16, it's Java 17). + +- GH-3490: Instance sorting is now aware of numbers (and sorts 99 before 100). + +- GH-4164: Reimplemented assigning instances to groups using drag & drop. + +- GH-1795: Added terminal launch option to use a specific Minecraft profile (in-game player name). + + Used like this: + ``` + ./MultiMC --launch 1.17.1 --profile MultiMCTest --server mc.hypixel.net + ``` + +- GH-4227: Fix crash related to invalid Forge mod metadata. + +- GH-4200: Search for the *Eclipse Foundation* and *Adoptium* Java runtimes in the Windows Registry. + +- Added shader packs page to instances. + +- Removed Mojang services status information from the main window - the status is no longer provided by Mojang. + +- It is now possible to turn of global tracking of play time. + +### Technical changes + +- Debranding is mostly finished. You may see some changes in the logo being used in less places. + +# Previous releases + +## MultiMC 0.6.13 This release brings initial support for Microsoft accounts, along with a nice pile of modpack platform support changes and improved Java runtime detection. @@ -6,7 +71,7 @@ Java runtimes still need an overhaul, so we're staying on the 0.6 version for a Next release should also tackle the current Forge 1.17.x issues in a systematic way. -### Microsoft accounts +#### Microsoft accounts This is the first release with Microsoft accounts in. @@ -24,7 +89,7 @@ As part of this, the skin fetching no longer uses a third party service and inst Capes can also be selected in MultiMC now. With how many people will now get one for migrating their accounts, it only makes sense. -### macOS update +#### macOS update Because of issues with the Microsoft accounts, we now have two builds on macOS: @@ -36,7 +101,7 @@ MultiMC will update to the 5.15.2 builds when it detects that this is possible. Similar approach got attempted on Windows, aiming to fix various display scaling and theming issues, but it ran into too many problems and will be attempted later, with more caution. -### Modpack platforms +#### Modpack platforms In general, the modpack platform pages have been made more consistent with each other (GH-3118, GH-3720, GH-3731). @@ -77,7 +142,7 @@ In general, the modpack platform pages have been made more consistent with each - Fixed bugs in FTB platform search. -### Other changes +#### Other changes - Forge installation is disabled on Minecraft 1.17+ because of incompatible/unresolved changes on the Forge side. @@ -117,11 +182,10 @@ In general, the modpack platform pages have been made more consistent with each - Quick and dirty minimum Java runtime versions checks have been added. This needs to be expanded in the future. -### Technical changes +#### Technical changes - The codebase continues to move towards being debranded and harder to build as 'MultiMC' for third parties. -# Previous releases ## MultiMC 0.6.12 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 37724038..ec6d5984 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -322,14 +322,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { showFatalErrorMessage( "The launcher data folder could not be created.", - "The launcher data folder could not be created.\n" - "\n" + QString( + "The launcher data folder could not be created.\n" + "\n" #if defined(Q_OS_MAC) - MACOS_HINT + MACOS_HINT #endif - "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" - "\n" - "The launcher cannot continue until you fix this problem." + "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem." + ).arg(dataPath); ); return; } @@ -337,14 +340,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { showFatalErrorMessage( "The launcher data folder could not be opened.", - "The launcher data folder could not be opened.\n" - "\n" + QString( + "The launcher data folder could not be opened.\n" + "\n" #if defined(Q_OS_MAC) - MACOS_HINT + MACOS_HINT #endif - "Make sure you have the right permissions to the launcher data folder.\n" - "\n" - "The launcher cannot continue until you fix this problem." + "Make sure you have the right permissions to the launcher data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem." + ).arg(dataPath); ); return; } @@ -494,14 +500,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { showFatalErrorMessage( "The launcher data folder is not writable!", - "The launcher couldn't create a log file - the data folder is not writable.\n" - "\n" + QString( + "The launcher couldn't create a log file - the data folder is not writable.\n" + "\n" #if defined(Q_OS_MAC) - MACOS_HINT + MACOS_HINT #endif - "Make sure you have write permissions to the data folder.\n" - "\n" - "The launcher cannot continue until you fix this problem." + "Make sure you have write permissions to the data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem." + ).arg(dataPath); ); return; } @@ -827,6 +836,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); + m_accounts->fillQueue(); qDebug() << "<> Accounts loaded."; } diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index fd26bb4f..488f2781 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -100,6 +100,9 @@ public: return instanceRoot(); } + /// Path to the instance's mods directory. + virtual QString modsRoot() const = 0; + QString name() const; void setName(QString val); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 08c878d1..2dfc78b5 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -196,36 +196,52 @@ set(ICONS_SOURCES # Support for Minecraft instances and launch set(MINECRAFT_SOURCES # Minecraft support - minecraft/auth/AccountData.h minecraft/auth/AccountData.cpp - minecraft/auth/AccountTask.h + minecraft/auth/AccountData.h + minecraft/auth/AccountList.cpp + minecraft/auth/AccountList.h minecraft/auth/AccountTask.cpp - minecraft/auth/AuthSession.h + minecraft/auth/AccountTask.h + minecraft/auth/AuthRequest.cpp + minecraft/auth/AuthRequest.h minecraft/auth/AuthSession.cpp - minecraft/auth/AccountList.h - minecraft/auth/AccountList.cpp - minecraft/auth/MinecraftAccount.h + minecraft/auth/AuthSession.h + minecraft/auth/AuthStep.cpp + minecraft/auth/AuthStep.h minecraft/auth/MinecraftAccount.cpp - minecraft/auth/flows/AuthContext.h - minecraft/auth/flows/AuthContext.cpp - minecraft/auth/flows/AuthRequest.h - minecraft/auth/flows/AuthRequest.cpp - - minecraft/auth/flows/MSAInteractive.h - minecraft/auth/flows/MSAInteractive.cpp - minecraft/auth/flows/MSASilent.h - minecraft/auth/flows/MSASilent.cpp - - minecraft/auth/flows/MojangLogin.h - minecraft/auth/flows/MojangLogin.cpp - minecraft/auth/flows/MojangRefresh.h - minecraft/auth/flows/MojangRefresh.cpp - - minecraft/auth/flows/Yggdrasil.h - minecraft/auth/flows/Yggdrasil.cpp - - minecraft/auth/flows/Parsers.h - minecraft/auth/flows/Parsers.cpp + minecraft/auth/MinecraftAccount.h + minecraft/auth/Parsers.cpp + minecraft/auth/Parsers.h + minecraft/auth/Yggdrasil.cpp + minecraft/auth/Yggdrasil.h + + minecraft/auth/flows/AuthFlow.cpp + minecraft/auth/flows/AuthFlow.h + minecraft/auth/flows/Mojang.cpp + minecraft/auth/flows/Mojang.h + minecraft/auth/flows/MSA.cpp + minecraft/auth/flows/MSA.h + + minecraft/auth/steps/EntitlementsStep.cpp + minecraft/auth/steps/EntitlementsStep.h + minecraft/auth/steps/GetSkinStep.cpp + minecraft/auth/steps/GetSkinStep.h + minecraft/auth/steps/LauncherLoginStep.cpp + minecraft/auth/steps/LauncherLoginStep.h + minecraft/auth/steps/MigrationEligibilityStep.cpp + minecraft/auth/steps/MigrationEligibilityStep.h + minecraft/auth/steps/MinecraftProfileStep.cpp + minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/MSAStep.cpp + minecraft/auth/steps/MSAStep.h + minecraft/auth/steps/XboxAuthorizationStep.cpp + minecraft/auth/steps/XboxAuthorizationStep.h + minecraft/auth/steps/XboxProfileStep.cpp + minecraft/auth/steps/XboxProfileStep.h + minecraft/auth/steps/XboxUserStep.cpp + minecraft/auth/steps/XboxUserStep.h + minecraft/auth/steps/YggdrasilStep.cpp + minecraft/auth/steps/YggdrasilStep.h minecraft/gameoptions/GameOptions.h minecraft/gameoptions/GameOptions.cpp diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 8bd5732f..be12eb79 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -35,6 +35,8 @@ void LaunchController::executeTask() return; } + JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); + login(); } @@ -90,8 +92,6 @@ void LaunchController::decideAccount() void LaunchController::login() { - JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); - decideAccount(); // if no account is selected, we bail @@ -113,120 +113,107 @@ void LaunchController::login() { { m_session = std::make_shared<AuthSession>(); m_session->wants_online = m_online; - shared_qobject_ptr<AccountTask> task; - if(!password.isNull()) { - task = m_accountToUse->login(m_session, password); - } - else { - task = m_accountToUse->refresh(m_session); - } - if (task) - { - // We'll need to validate the access token to make sure the account - // is still logged in. - ProgressDialog progDialog(m_parentWidget); - if (m_online) - { - progDialog.setSkipButton(true, tr("Play Offline")); - } - progDialog.execWithTask(task.get()); - if (!task->wasSuccessful()) - { - auto failReasonNew = task->failReason(); - if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature") - { - // account->invalidateClientToken(); - failReason = needLoginAgain; - } - else failReason = failReasonNew; - } - } - switch (m_session->status) - { - case AuthSession::Undetermined: { - qCritical() << "Received undetermined session status during login. Bye."; - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); - return; + m_accountToUse->fillSession(m_session); + + switch(m_accountToUse->accountState()) { + case AccountState::Offline: { + m_session->wants_online = false; + // NOTE: fallthrough is intentional } - case AuthSession::RequiresPassword: { - // FIXME: this needs to understand MSA - EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField); - auto username = m_session->username; - auto chopN = [](QString toChop, int N) -> QString - { - if(toChop.size() > N) + case AccountState::Online: { + if(!m_session->wants_online) { + // we ask the user for a player name + bool ok = false; + QString usedname = m_session->player_name; + QString name = QInputDialog::getText( + m_parentWidget, + tr("Player name"), + tr("Choose your offline mode player name."), + QLineEdit::Normal, + m_session->player_name, + &ok + ); + if (!ok) { - auto left = toChop.left(N); - left += QString("\u25CF").repeated(toChop.size() - N); - return left; + tryagain = false; + break; } - return toChop; - }; - - if(username.contains('@')) - { - auto parts = username.split('@'); - auto mailbox = chopN(parts[0],3); - QString domain = chopN(parts[1], 3); - username = mailbox + '@' + domain; + if (name.length()) + { + usedname = name; + } + m_session->MakeOffline(usedname); + // offline flavored game from here :3 } - passDialog.setUsername(username); - if (passDialog.exec() == QDialog::Accepted) - { - password = passDialog.password(); + if(m_accountToUse->ownsMinecraft() && !m_accountToUse->hasProfile()) { + auto entitlement = m_accountToUse->accountData()->minecraftEntitlement; + QString errorString; + if(!entitlement.canPlayMinecraft) { + errorString = tr("The account does not own Minecraft. You need to purchase the game first to play it."); + QMessageBox::warning( + nullptr, + tr("Missing Minecraft profile"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } + // 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; + } } - else - { - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); + else { + launchInstance(); } - break; + return; } - case AuthSession::RequiresProfileSetup: { - auto entitlement = m_accountToUse->accountData()->minecraftEntitlement; - QString errorString; - if(!entitlement.canPlayMinecraft) { - errorString = tr("The account does not own Minecraft. You need to purchase the game first to play it."); - QMessageBox::warning( - nullptr, - tr("Missing Minecraft profile"), - errorString, - QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok - ); - tryagain = false; - emitFailed(errorString); - return; - } - // Now handle setting up a profile name here... - ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); - if (dialog.exec() == QDialog::Accepted) - { - tryagain = true; - continue; - } - else + case AccountState::Errored: + // This means some sort of soft error that we can fix with a refresh ... so let's refresh. + case AccountState::Unchecked: { + m_accountToUse->refresh(); + // NOTE: fallthrough intentional + } + case AccountState::Working: { + // refresh is in progress, we need to wait for it to finish to proceed. + ProgressDialog progDialog(m_parentWidget); + if (m_online) { - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); - return; + progDialog.setSkipButton(true, tr("Play Offline")); } + auto task = m_accountToUse->currentTask(); + progDialog.execWithTask(task.get()); + continue; + } + // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that + /* + case AccountState::Queued: { + return; } - case AuthSession::RequiresOAuth: { - auto errorString = tr("Microsoft account has expired and needs to be logged into manually again."); + */ + case AccountState::Expired: { + auto errorString = tr("The account has expired and needs to be logged into manually again."); QMessageBox::warning( m_parentWidget, - tr("Microsoft Account refresh failed"), + tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok ); - tryagain = false; emitFailed(errorString); return; } - case AuthSession::GoneOrMigrated: { + case AccountState::Gone: { auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to."); QMessageBox::warning( m_parentWidget, @@ -235,40 +222,9 @@ void LaunchController::login() { QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok ); - tryagain = false; emitFailed(errorString); return; } - case AuthSession::PlayableOffline: { - // we ask the user for a player name - bool ok = false; - QString usedname = m_session->player_name; - QString name = QInputDialog::getText( - m_parentWidget, - tr("Player name"), - tr("Choose your offline mode player name."), - QLineEdit::Normal, - m_session->player_name, - &ok - ); - if (!ok) - { - tryagain = false; - break; - } - if (name.length()) - { - usedname = name; - } - m_session->MakeOffline(usedname); - // offline flavored game from here :3 - } - case AuthSession::PlayableOnline: - { - launchInstance(); - tryagain = false; - return; - } } } emitFailed(tr("Failed to launch.")); @@ -334,14 +290,7 @@ void LaunchController::launchInstance() online_mode = "offline"; } - QString auth_server_status; - if(m_session->auth_server_online) { - auth_server_status = "online"; - } else { - auth_server_status = "offline"; - } - - m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\nAuthentication server is " + auth_server_status + "\n", MessageLevel::Launcher)); + m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_NAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index fa26e0b9..4d7f424d 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2015 Petr Mrazek <peterix@gmail.com> + * Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <MMCTime.h> #include <QObject> diff --git a/launcher/MMCTime.h b/launcher/MMCTime.h index 728a5abb..10ff2ffe 100644 --- a/launcher/MMCTime.h +++ b/launcher/MMCTime.h @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 94ed6c3a..ed421433 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -73,4 +73,7 @@ public: out << "Null instance - placeholder."; return out; } + QString modsRoot() const override { + return QString(); + } }; diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 6b58db37..c844fb94 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -265,13 +265,17 @@ QList<QString> JavaUtils::FindJavaPaths() QList<JavaInstallPtr> ADOPTOPENJDK64s = this->FindJavaFromRegistryKey( KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); - // Foundation (Eclipse) + // Eclipse Foundation QList<JavaInstallPtr> FOUNDATIONJDK32s = this->FindJavaFromRegistryKey( KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); QList<JavaInstallPtr> FOUNDATIONJDK64s = this->FindJavaFromRegistryKey( KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); - // Adoptium (Eclipse) + // Eclipse Adoptium + QList<JavaInstallPtr> ADOPTIUMJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTIUMJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); QList<JavaInstallPtr> ADOPTIUMJDK32s = this->FindJavaFromRegistryKey( KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); QList<JavaInstallPtr> ADOPTIUMJDK64s = this->FindJavaFromRegistryKey( @@ -297,6 +301,7 @@ QList<QString> JavaUtils::FindJavaPaths() java_candidates.append(JRE64s); java_candidates.append(NEWJRE64s); java_candidates.append(ADOPTOPENJRE64s); + java_candidates.append(ADOPTIUMJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); @@ -312,6 +317,7 @@ QList<QString> JavaUtils::FindJavaPaths() java_candidates.append(JRE32s); java_candidates.append(NEWJRE32s); java_candidates.append(ADOPTOPENJRE32s); + java_candidates.append(ADOPTIUMJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 4c16e572..2526e620 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -202,7 +202,7 @@ QString MinecraftInstance::jarModsDir() const return jarmods_dir.absolutePath(); } -QString MinecraftInstance::loaderModsDir() const +QString MinecraftInstance::modsRoot() const { return FS::PathCombine(gameRoot(), "mods"); } @@ -961,7 +961,7 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new ModFolderModel(loaderModsDir())); + m_loader_mod_list.reset(new ModFolderModel(modsRoot())); m_loader_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index bb45f37b..fda58aa7 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -40,7 +40,7 @@ public: QString resourcePacksDir() const; QString texturePacksDir() const; QString shaderPacksDir() const; - QString loaderModsDir() const; + QString modsRoot() const override; QString coreModsDir() const; QString modsCacheLocation() const; QString libDir() const; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 8aa4e37f..7526c951 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -438,3 +438,7 @@ QString AccountData::accountDisplayString() const { } } } + +QString AccountData::lastError() const { + return errorString; +} diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 09cd2c73..abf84e43 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -41,6 +41,16 @@ enum class AccountType { Mojang }; +enum class AccountState { + Unchecked, + Offline, + Working, + Online, + Errored, + Expired, + Gone +}; + struct AccountData { QJsonObject saveState() const; bool resumeStateFromV2(QJsonObject data); @@ -64,6 +74,8 @@ struct AccountData { QString profileId() const; QString profileName() const; + QString lastError() const; + AccountType type = AccountType::MSA; bool legacy = false; bool canMigrateToMSA = false; @@ -77,4 +89,9 @@ struct AccountData { MinecraftProfile minecraftProfile; MinecraftEntitlement minecraftEntitlement; Katabasis::Validity validity_ = Katabasis::Validity::None; + + // runtime only information (not saved with the account) + QString internalId; + QString errorString; + AccountState accountState = AccountState::Unchecked; }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index d7537345..ef8b435d 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -15,6 +15,7 @@ #include "AccountList.h" #include "AccountData.h" +#include "AccountTask.h" #include <QIODevice> #include <QFile> @@ -24,18 +25,28 @@ #include <QJsonObject> #include <QJsonParseError> #include <QDir> +#include <QTimer> #include <QDebug> #include <FileSystem.h> #include <QSaveFile> +#include <chrono> + enum AccountListVersion { MojangOnly = 2, MojangMSA = 3 }; -AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { } +AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); + m_nextTimer = new QTimer(this); + m_nextTimer->setSingleShot(true); + connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); +} AccountList::~AccountList() noexcept {} @@ -78,9 +89,18 @@ QStringList AccountList::profileNames() const { void AccountList::addAccount(const MinecraftAccountPtr account) { + // NOTE: Do not allow adding something that's already there + if(m_accounts.contains(account)) { + return; + } + + // hook up notifications for changes in the account + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + + // override/replace existing account with the same profileId auto profileId = account->profileId(); if(profileId.size()) { - // override/replace existing account with the same profileId auto existingAccount = findAccountByProfileId(profileId); if(existingAccount != -1) { MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; @@ -88,6 +108,8 @@ void AccountList::addAccount(const MinecraftAccountPtr account) if(m_defaultAccount == existingAccountPtr) { m_defaultAccount = account; } + // disconnect notifications for changes in the account being replaced + existingAccountPtr->disconnect(this); emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); onListChanged(); return; @@ -97,8 +119,6 @@ void AccountList::addAccount(const MinecraftAccountPtr account) // if we don't have this profileId yet, add the account to the end int row = m_accounts.count(); beginInsertRows(QModelIndex(), row, row); - connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); - connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); m_accounts.append(account); endInsertRows(); onListChanged(); @@ -115,6 +135,8 @@ void AccountList::removeAccount(QModelIndex index) m_defaultAccount = nullptr; onDefaultAccountChanged(); } + account->disconnect(this); + beginRemoveRows(QModelIndex(), row, row); m_accounts.removeAt(index.row()); endRemoveRows(); @@ -193,6 +215,12 @@ void AccountList::accountActivityChanged(bool active) } if(found) { emit listActivityChanged(); + if(active) { + beginActivity(); + } + else { + endActivity(); + } } } @@ -244,13 +272,29 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case StatusColumn: { - if(account->isActive()) { - return tr("Working", "Account status"); - } - if(account->isExpired()) { - return tr("Expired", "Account status"); + switch(account->accountState()) { + case AccountState::Unchecked: { + return tr("Unchecked", "Account status"); + } + case AccountState::Offline: { + return tr("Offline", "Account status"); + } + case AccountState::Online: { + return tr("Online", "Account status"); + } + case AccountState::Working: { + return tr("Working", "Account status"); + } + case AccountState::Errored: { + return tr("Errored", "Account status"); + } + case AccountState::Expired: { + return tr("Expired", "Account status"); + } + case AccountState::Gone: { + return tr("Gone", "Account status"); + } } - return tr("Ready", "Account status"); } case ProfileNameColumn: { @@ -583,10 +627,113 @@ void AccountList::setListFilePath(QString path, bool autosave) bool AccountList::anyAccountIsValid() { - for(auto account:m_accounts) + for(auto account: m_accounts) { - if(account->accountStatus() != NotVerified) + if(account->ownsMinecraft()) { return true; + } } return false; } + +void AccountList::fillQueue() { + + if(m_defaultAccount && m_defaultAccount->shouldRefresh()) { + auto idToRefresh = m_defaultAccount->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + } + + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account == m_defaultAccount) { + continue; + } + + if(account->shouldRefresh()) { + auto idToRefresh = account->internalId(); + queueRefresh(idToRefresh); + } + } + tryNext(); +} + +void AccountList::requestRefresh(QString accountId) { + auto index = m_refreshQueue.indexOf(accountId); + if(index != -1) { + m_refreshQueue.removeAt(index); + } + m_refreshQueue.push_front(accountId); + qDebug() << "AccountList: Pushed account with internal ID " << accountId << " to the front of the queue"; + if(!isActive()) { + tryNext(); + } +} + +void AccountList::queueRefresh(QString accountId) { + if(m_refreshQueue.indexOf(accountId) != -1) { + return; + } + m_refreshQueue.push_back(accountId); + qDebug() << "AccountList: Queued account with internal ID " << accountId << " to refresh"; +} + + +void AccountList::tryNext() { + while (m_refreshQueue.length()) { + auto accountId = m_refreshQueue.front(); + m_refreshQueue.pop_front(); + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account->internalId() == accountId) { + m_currentTask = account->refresh(); + if(m_currentTask) { + connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed); + m_currentTask->start(); + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId; + return; + } + } + } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + } + // if we get here, no account needed refreshing. Schedule refresh in an hour. + m_refreshTimer->start(1000 * 3600); +} + +void AccountList::authSucceeded() { + qDebug() << "RefreshSchedule: Background account refresh succeeded"; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +void AccountList::authFailed(QString reason) { + qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +bool AccountList::isActive() const { + return m_activityCount != 0; +} + +void AccountList::beginActivity() { + bool activating = m_activityCount == 0; + m_activityCount++; + if(activating) { + emit activityChanged(true); + } +} + +void AccountList::endActivity() { + if(m_activityCount == 0) { + qWarning() << m_name << " - Activity count would become below zero"; + return; + } + bool deactivating = m_activityCount == 1; + m_activityCount--; + if(deactivating) { + emit activityChanged(false); + } +} diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 08004628..fa1e7431 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -67,6 +67,11 @@ public: MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const; QStringList profileNames() const; + // requesting a refresh pushes it to the front of the queue + void requestRefresh(QString accountId); + // queuing a refresh will let it go to the back of the queue (unless it's somewhere inside the queue already) + void queueRefresh(QString accountId); + /*! * Sets the path to load/save the list file from/to. * If autosave is true, this list will automatically save to the given path whenever it changes. @@ -85,10 +90,20 @@ public: void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); + bool isActive() const; + +protected: + void beginActivity(); + void endActivity(); + +private: + const char* m_name; + uint32_t m_activityCount = 0; signals: void listChanged(); void listActivityChanged(); void defaultAccountChanged(); + void activityChanged(bool active); public slots: /** @@ -101,7 +116,23 @@ public slots: */ void accountActivityChanged(bool active); + /** + * This is initially to run background account refresh tasks, or on a hourly timer + */ + void fillQueue(); + +private slots: + void tryNext(); + + void authSucceeded(); + void authFailed(QString reason); + protected: + QList<QString> m_refreshQueue; + QTimer *m_refreshTimer; + QTimer *m_nextTimer; + shared_qobject_ptr<AccountTask> m_currentTask; + /*! * Called whenever the list changes. * This emits the listChanged() signal and autosaves the list (if autosave is enabled). diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 25d753de..98d8d94d 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -28,40 +28,79 @@ AccountTask::AccountTask(AccountData *data, QObject *parent) : Task(parent), m_data(data) { - changeState(STATE_CREATED); + changeState(AccountTaskState::STATE_CREATED); } QString AccountTask::getStateMessage() const { - switch (m_accountState) + switch (m_taskState) { - case STATE_CREATED: + case AccountTaskState::STATE_CREATED: return "Waiting..."; - case STATE_WORKING: + case AccountTaskState::STATE_WORKING: return tr("Sending request to auth servers..."); - case STATE_SUCCEEDED: + case AccountTaskState::STATE_SUCCEEDED: return tr("Authentication task succeeded."); - case STATE_FAILED_SOFT: + case AccountTaskState::STATE_OFFLINE: return tr("Failed to contact the authentication server."); - case STATE_FAILED_HARD: - return tr("Failed to authenticate."); - case STATE_FAILED_GONE: + case AccountTaskState::STATE_FAILED_SOFT: + return tr("Encountered an error during authentication."); + case AccountTaskState::STATE_FAILED_HARD: + return tr("Failed to authenticate. The session has expired."); + case AccountTaskState::STATE_FAILED_GONE: return tr("Failed to authenticate. The account no longer exists."); default: return tr("..."); } } -void AccountTask::changeState(AccountTask::State newState, QString reason) +bool AccountTask::changeState(AccountTaskState newState, QString reason) { - m_accountState = newState; + m_taskState = newState; setStatus(getStateMessage()); - if (newState == STATE_SUCCEEDED) - { - emitSucceeded(); - } - else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE) - { - emitFailed(reason); + switch(newState) { + case AccountTaskState::STATE_CREATED: { + m_data->errorString.clear(); + return true; + } + case AccountTaskState::STATE_WORKING: { + m_data->accountState = AccountState::Working; + return true; + } + case AccountTaskState::STATE_SUCCEEDED: { + m_data->accountState = AccountState::Online; + emitSucceeded(); + return false; + } + case AccountTaskState::STATE_OFFLINE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Offline; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_SOFT: { + m_data->errorString = reason; + m_data->accountState = AccountState::Errored; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_HARD: { + m_data->errorString = reason; + m_data->accountState = AccountState::Expired; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_GONE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Gone; + emitFailed(reason); + return false; + } + default: { + QString error = tr("Unknown account task state: %1").arg(int(newState)); + m_data->accountState = AccountState::Errored; + emitFailed(error); + return false; + } } } diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index 4f3bd52a..dac3f1b5 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -26,62 +26,32 @@ class QNetworkReply; +/** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ +enum class AccountTaskState +{ + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_FAILED_SOFT, //!< soft failure. authentication went through partially + STATE_FAILED_HARD, //!< hard failure. main tokens are invalid + STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists + STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way +}; + class AccountTask : public Task { - friend class AuthContext; Q_OBJECT public: explicit AccountTask(AccountData * data, QObject *parent = 0); virtual ~AccountTask() {}; - /** - * assign a session to this task. the session will be filled with required infomration - * upon completion - */ - void assignSession(AuthSessionPtr session) - { - m_session = session; - } - - /// get the assigned session for filling with information. - AuthSessionPtr getAssignedSession() - { - return m_session; - } - - /** - * Class describing a Account error response. - */ - struct Error - { - QString m_errorMessageShort; - QString m_errorMessageVerbose; - QString m_cause; - }; - - enum AbortedBy - { - BY_NOTHING, - BY_USER, - BY_TIMEOUT - } m_aborted = BY_NOTHING; - - /** - * Enum for describing the state of the current task. - * Used by the getStateMessage function to determine what the status message should be. - */ - enum State - { - STATE_CREATED, - STATE_WORKING, - STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated - STATE_FAILED_HARD, //!< hard failure. auth is invalid - STATE_FAILED_GONE, //!< hard failure. auth is invalid, and the account no longer exists - STATE_SUCCEEDED - } m_accountState = STATE_CREATED; + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; - State accountState() { - return m_accountState; + AccountTaskState taskState() { + return m_taskState; } signals: @@ -98,11 +68,9 @@ protected: virtual QString getStateMessage() const; protected slots: - void changeState(State newState, QString reason=QString()); + // NOTE: true -> non-terminal state, false -> terminal state + bool changeState(AccountTaskState newState, QString reason = QString()); protected: - // FIXME: segfault disaster waiting to happen AccountData *m_data = nullptr; - std::shared_ptr<Error> m_error; - AuthSessionPtr m_session; }; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp index 82dba591..459d2354 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.cpp +++ b/launcher/minecraft/auth/AuthRequest.cpp @@ -44,6 +44,7 @@ void AuthRequest::onRequestFinished() { if (reply_ != qobject_cast<QNetworkReply *>(sender())) { return; } + httpStatus_ = 200; finish(); } @@ -55,10 +56,11 @@ void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { if (reply_ != qobject_cast<QNetworkReply *>(sender())) { return; } - qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString(); - int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + errorString_ = reply_->errorString(); + httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); error_ = error; + qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_; + qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); // QTimer::singleShot(10, this, SLOT(finish())); } @@ -103,6 +105,8 @@ void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Opera status_ = Requesting; error_ = QNetworkReply::NoError; + errorString_.clear(); + httpStatus_ = 0; } void AuthRequest::finish() { diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/AuthRequest.h index a547aea4..89f7a123 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.h +++ b/launcher/minecraft/auth/AuthRequest.h @@ -46,6 +46,11 @@ protected slots: /// Handle upload progress. void onUploadProgress(qint64 uploaded, qint64 total); +public: + QNetworkReply::NetworkError error_; + int httpStatus_ = 0; + QString errorString_; + protected: void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); @@ -60,5 +65,6 @@ protected: QNetworkAccessManager::Operation operation_; QUrl url_; Katabasis::ReplyList timedReplies_; - QNetworkReply::NetworkError error_; + + QTimer *timer_; }; diff --git a/launcher/minecraft/auth/AuthStep.cpp b/launcher/minecraft/auth/AuthStep.cpp new file mode 100644 index 00000000..ffa2581b --- /dev/null +++ b/launcher/minecraft/auth/AuthStep.cpp @@ -0,0 +1,7 @@ +#include "AuthStep.h" + +AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) { +} + +AuthStep::~AuthStep() noexcept = default; + diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h new file mode 100644 index 00000000..2a8dc2ca --- /dev/null +++ b/launcher/minecraft/auth/AuthStep.h @@ -0,0 +1,33 @@ +#pragma once +#include <QObject> +#include <QList> +#include <QNetworkReply> + +#include "QObjectPtr.h" +#include "minecraft/auth/AccountData.h" +#include "AccountTask.h" + +class AuthStep : public QObject { + Q_OBJECT + +public: + using Ptr = shared_qobject_ptr<AuthStep>; + +public: + explicit AuthStep(AccountData *data); + virtual ~AuthStep() noexcept; + + virtual QString describe() = 0; + +public slots: + virtual void perform() = 0; + virtual void rehydrate() = 0; + +signals: + void finished(AccountTaskState resultingState, QString message); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + +protected: + AccountData *m_data; +}; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 30ed6afe..ed9e945e 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -16,7 +16,6 @@ */ #include "MinecraftAccount.h" -#include "flows/AuthContext.h" #include <QUuid> #include <QJsonObject> @@ -28,14 +27,12 @@ #include <QDebug> #include <QPainter> -#include "flows/MSASilent.h" -#include "flows/MSAInteractive.h" -#include "flows/MojangRefresh.h" -#include "flows/MojangLogin.h" +#include "flows/MSA.h" +#include "flows/Mojang.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); } @@ -77,42 +74,10 @@ QJsonObject MinecraftAccount::saveToJson() const return data.saveState(); } -AccountStatus MinecraftAccount::accountStatus() const { - if(data.type == AccountType::Mojang) { - if (data.accessToken().isEmpty()) { - return NotVerified; - } - else { - return Verified; - } - } - // MSA - // FIXME: this is extremely crude and probably wrong - if(data.msaToken.token.isEmpty()) { - return NotVerified; - } - else { - return Verified; - } -} - -bool MinecraftAccount::isExpired() const { - switch(data.type) { - case AccountType::Mojang: { - return data.accessToken().isEmpty(); - } - break; - case AccountType::MSA: { - return data.msaToken.validity == Katabasis::Validity::None; - } - break; - default: { - return true; - } - } +AccountState MinecraftAccount::accountState() const { + return data.accountState; } - QPixmap MinecraftAccount::getFace() const { QPixmap skinTexture; if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { @@ -126,136 +91,51 @@ QPixmap MinecraftAccount::getFace() const { } -shared_qobject_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password) -{ +shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) { Q_ASSERT(m_currentTask.get() == nullptr); - // take care of the true offline status - if (accountStatus() == NotVerified && password.isEmpty()) - { - if (session) - { - session->status = AuthSession::RequiresPassword; - fillSession(session); - } - return nullptr; - } - - if(accountStatus() == Verified && !session->wants_online) - { - session->status = AuthSession::PlayableOffline; - session->auth_server_online = false; - fillSession(session); - return nullptr; - } - else - { - if (password.isEmpty()) - { - m_currentTask.reset(new MojangRefresh(&data)); - } - else - { - m_currentTask.reset(new MojangLogin(&data, password)); - } - m_currentTask->assignSession(session); - - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); - emit activityChanged(true); - } + m_currentTask.reset(new MojangLogin(&data, password)); + 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<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) { +shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() { Q_ASSERT(m_currentTask.get() == nullptr); - if(accountStatus() == Verified && !session->wants_online) - { - session->status = AuthSession::PlayableOffline; - session->auth_server_online = false; - fillSession(session); - return nullptr; - } - else - { - m_currentTask.reset(new MSAInteractive(&data)); - m_currentTask->assignSession(session); - - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); - emit activityChanged(true); - } + m_currentTask.reset(new MSAInteractive(&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<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) { - Q_ASSERT(m_currentTask.get() == nullptr); - - // take care of the true offline status - if (accountStatus() == NotVerified) - { - if (session) - { - if(data.type == AccountType::MSA) { - session->status = AuthSession::RequiresOAuth; - } - else { - session->status = AuthSession::RequiresPassword; - } - fillSession(session); - } - return nullptr; +shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() { + if(m_currentTask) { + return m_currentTask; } - if(accountStatus() == Verified && !session->wants_online) - { - session->status = AuthSession::PlayableOffline; - session->auth_server_online = false; - fillSession(session); - return nullptr; + if(data.type == AccountType::MSA) { + m_currentTask.reset(new MSASilent(&data)); } - else - { - if(data.type == AccountType::MSA) { - m_currentTask.reset(new MSASilent(&data)); - } - else { - m_currentTask.reset(new MojangRefresh(&data)); - } - m_currentTask->assignSession(session); - - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); - emit activityChanged(true); + else { + m_currentTask.reset(new MojangRefresh(&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<AccountTask> MinecraftAccount::currentTask() { return m_currentTask; } void MinecraftAccount::authSucceeded() { - auto session = m_currentTask->getAssignedSession(); - if (session) - { - /* - session->status = AuthSession::RequiresProfileSetup; - session->auth_server_online = true; - */ - if(data.profileId().size() == 0) { - session->status = AuthSession::RequiresProfileSetup; - } - else { - if(session->wants_online) { - session->status = AuthSession::PlayableOnline; - } - else { - session->status = AuthSession::PlayableOffline; - } - } - fillSession(session); - session->auth_server_online = true; - } m_currentTask.reset(); emit changed(); emit activityChanged(false); @@ -263,62 +143,35 @@ void MinecraftAccount::authSucceeded() void MinecraftAccount::authFailed(QString reason) { - auto session = m_currentTask->getAssignedSession(); - // This is emitted when the yggdrasil tasks time out or are cancelled. - // -> we treat the error as no-op - switch (m_currentTask->accountState()) { - case AccountTask::STATE_FAILED_SOFT: { - if (session) - { - if(accountStatus() == Verified) { - session->status = AuthSession::PlayableOffline; - } - else { - if(data.type == AccountType::MSA) { - session->status = AuthSession::RequiresOAuth; - } - else { - session->status = AuthSession::RequiresPassword; - } - } - session->auth_server_online = false; - fillSession(session); - } + switch (m_currentTask->taskState()) { + case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_FAILED_SOFT: { + // NOTE: this doesn't do much. There was an error of some sort. } break; - case AccountTask::STATE_FAILED_HARD: { - // FIXME: MSA data clearing - data.yggdrasilToken.token = QString(); - data.yggdrasilToken.validity = Katabasis::Validity::None; - data.validity_ = Katabasis::Validity::None; - emit changed(); - if (session) - { - if(data.type == AccountType::MSA) { - session->status = AuthSession::RequiresOAuth; - } - else { - session->status = AuthSession::RequiresPassword; - } - session->auth_server_online = true; - fillSession(session); + case AccountTaskState::STATE_FAILED_HARD: { + if(isMSA()) { + data.msaToken.token = QString(); + data.msaToken.refresh_token = QString(); + data.msaToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + } + else { + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; } + emit changed(); } break; - case AccountTask::STATE_FAILED_GONE: { + case AccountTaskState::STATE_FAILED_GONE: { data.validity_ = Katabasis::Validity::None; emit changed(); - if (session) - { - session->status = AuthSession::GoneOrMigrated; - session->auth_server_online = true; - fillSession(session); - } } break; - case AccountTask::STATE_CREATED: - case AccountTask::STATE_WORKING: - case AccountTask::STATE_SUCCEEDED: { + case AccountTaskState::STATE_CREATED: + case AccountTaskState::STATE_WORKING: + case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } } @@ -358,7 +211,7 @@ bool MinecraftAccount::shouldRefresh() const { if(!expiresTimestamp.isValid()) { expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); } - if (now.secsTo(expiresTimestamp) < 12 * 3600) { + if (now.secsTo(expiresTimestamp) < (12 * 3600)) { return true; } return false; @@ -366,6 +219,18 @@ bool MinecraftAccount::shouldRefresh() const { void MinecraftAccount::fillSession(AuthSessionPtr session) { + if(ownsMinecraft() && !hasProfile()) { + session->status = AuthSession::RequiresProfileSetup; + } + else { + if(session->wants_online) { + session->status = AuthSession::PlayableOnline; + } + else { + session->status = AuthSession::PlayableOffline; + } + } + // the user name. you have to have an user name // FIXME: not with MSA session->username = data.userName(); diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 459ef903..4ac0a3e5 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -24,6 +24,7 @@ #include <QPixmap> #include <memory> + #include "AuthSession.h" #include "Usable.h" #include "AccountData.h" @@ -50,12 +51,6 @@ struct AccountProfile bool legacy; }; -enum AccountStatus -{ - NotVerified, - Verified -}; - /** * Object that stores information about a certain Mojang account. * @@ -90,15 +85,17 @@ public: /* manipulation */ * Attempt to login. Empty password means we use the token. * If the attempt fails because we already are performing some task, it returns false. */ - shared_qobject_ptr<AccountTask> login(AuthSessionPtr session, QString password); + shared_qobject_ptr<AccountTask> login(QString password); - shared_qobject_ptr<AccountTask> loginMSA(AuthSessionPtr session); + shared_qobject_ptr<AccountTask> loginMSA(); - shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session); + shared_qobject_ptr<AccountTask> refresh(); + + shared_qobject_ptr<AccountTask> currentTask(); public: /* queries */ QString internalId() const { - return m_internalId; + return data.internalId; } QString accountDisplayString() const { @@ -123,8 +120,6 @@ public: /* queries */ bool isActive() const; - bool isExpired() const; - bool canMigrate() const { return data.canMigrateToMSA; } @@ -133,6 +128,14 @@ public: /* queries */ return data.type == AccountType::MSA; } + bool ownsMinecraft() const { + return data.minecraftEntitlement.ownsMinecraft; + } + + bool hasProfile() const { + return data.profileId().size() != 0; + } + QString typeString() const { switch(data.type) { case AccountType::Mojang: { @@ -154,8 +157,8 @@ public: /* queries */ QPixmap getFace() const; - //! Returns whether the account is NotVerified, Verified or Online - AccountStatus accountStatus() const; + //! Returns the current state of the account + AccountState accountState() const; AccountData * accountData() { return &data; @@ -163,6 +166,12 @@ public: /* queries */ bool shouldRefresh() const; + void fillSession(AuthSessionPtr session); + + QString lastError() const { + return data.lastError(); + } + signals: /** * This signal is emitted when the account changes @@ -174,7 +183,6 @@ signals: // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ - QString m_internalId; AccountData data; // current task we are executing here @@ -189,7 +197,4 @@ private slots: void authSucceeded(); void authFailed(QString reason); - -private: - void fillSession(AuthSessionPtr session); }; diff --git a/launcher/minecraft/auth/flows/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index ecb11cf9..4cab78ef 100644 --- a/launcher/minecraft/auth/flows/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -72,7 +72,7 @@ bool getBool(QJsonValue value, bool & out) { // 2148916238 = child account not linked to a family */ -bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) { qDebug() << "Parsing" << name <<":"; #ifndef NDEBUG qDebug() << data; diff --git a/launcher/minecraft/auth/flows/Parsers.h b/launcher/minecraft/auth/Parsers.h index b484a073..dac7f69b 100644 --- a/launcher/minecraft/auth/flows/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -1,6 +1,6 @@ #pragma once -#include "../AccountData.h" +#include "AccountData.h" namespace Parsers { @@ -10,7 +10,7 @@ namespace Parsers bool getNumber(QJsonValue value, int64_t & out); bool getBool(QJsonValue value, bool & out); - bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name); + bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, QString name); bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 5ea168e8..7ac842a6 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -14,7 +14,7 @@ */ #include "Yggdrasil.h" -#include "../AccountData.h" +#include "AccountData.h" #include <QObject> #include <QString> @@ -30,11 +30,11 @@ Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) : AccountTask(data, parent) { - changeState(STATE_CREATED); + changeState(AccountTaskState::STATE_CREATED); } void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { - changeState(STATE_WORKING); + changeState(AccountTaskState::STATE_WORKING); QNetworkRequest netRequest(endpoint); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -185,14 +185,14 @@ void Yggdrasil::processResponse(QJsonObject responseData) { QString clientToken = responseData.value("clientToken").toString(""); if (clientToken.isEmpty()) { // Fail if the server gave us an empty client token - changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); return; } if(m_data->clientToken().isEmpty()) { m_data->setClientToken(clientToken); } else if(clientToken != m_data->clientToken()) { - changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); return; } @@ -201,7 +201,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) { QString accessToken = responseData.value("accessToken").toString(""); if (accessToken.isEmpty()) { // Fail if the server didn't give us an access token. - changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); return; } // Set the access token. @@ -212,25 +212,25 @@ void Yggdrasil::processResponse(QJsonObject responseData) { // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. qDebug() << "Finished reading authentication response."; - changeState(STATE_SUCCEEDED); + changeState(AccountTaskState::STATE_SUCCEEDED); } void Yggdrasil::processReply() { - changeState(STATE_WORKING); + changeState(AccountTaskState::STATE_WORKING); switch (m_netReply->error()) { case QNetworkReply::NoError: break; case QNetworkReply::TimeoutError: - changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out.")); + changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out.")); return; case QNetworkReply::OperationCanceledError: - changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); + changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); return; case QNetworkReply::SslHandshakeFailedError: changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr( "<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>" "<ul>" @@ -248,13 +248,13 @@ void Yggdrasil::processReply() { break; case QNetworkReply::ContentGoneError: { changeState( - STATE_FAILED_GONE, + AccountTaskState::STATE_FAILED_GONE, tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") ); } default: changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error()) ); return; @@ -279,7 +279,7 @@ void Yggdrasil::processReply() { } else { changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) ); qCritical() << replyData; @@ -303,7 +303,7 @@ void Yggdrasil::processReply() { // error. qDebug() << "The request failed and the server gave no error message. Unknown error."; changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()) ); } @@ -322,10 +322,10 @@ void Yggdrasil::processError(QJsonObject responseData) { causeVal.toString("") } ); - changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); + changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose); } else { // Error is not in standard format. Don't set m_error and return unknown error. - changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); } } diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h index b9670ec7..4f52a04c 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.h +++ b/launcher/minecraft/auth/Yggdrasil.h @@ -15,14 +15,14 @@ #pragma once -#include "../AccountTask.h" +#include "AccountTask.h" #include <QString> #include <QJsonObject> #include <QTimer> #include <qsslerror.h> -#include "../MinecraftAccount.h" +#include "MinecraftAccount.h" class QNetworkAccessManager; class QNetworkReply; @@ -38,10 +38,26 @@ public: AccountData *data, QObject *parent = 0 ); - virtual ~Yggdrasil() {}; + virtual ~Yggdrasil() = default; void refresh(); void login(QString password); + + struct Error + { + QString m_errorMessageShort; + QString m_errorMessageVerbose; + QString m_cause; + }; + std::shared_ptr<Error> m_error; + + enum AbortedBy + { + BY_NOTHING, + BY_USER, + BY_TIMEOUT + } m_aborted = BY_NOTHING; + protected: void executeTask() override; diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp deleted file mode 100644 index 00957fd4..00000000 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ /dev/null @@ -1,671 +0,0 @@ -#include <QNetworkAccessManager> -#include <QNetworkRequest> -#include <QNetworkReply> -#include <QDesktopServices> -#include <QMetaEnum> -#include <QDebug> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> -#include <QUuid> -#include <QUrlQuery> - -#include "AuthContext.h" -#include "katabasis/Globals.h" -#include "AuthRequest.h" - -#include "Parsers.h" - -#include <Application.h> - -using OAuth2 = Katabasis::DeviceFlow; -using Activity = Katabasis::Activity; - -AuthContext::AuthContext(AccountData * data, QObject *parent) : - AccountTask(data, parent) -{ -} - -void AuthContext::beginActivity(Activity activity) { - if(isBusy()) { - throw 0; - } - m_activity = activity; - changeState(STATE_WORKING, "Initializing"); - emit activityChanged(m_activity); -} - -void AuthContext::finishActivity() { - if(!isBusy()) { - throw 0; - } - m_activity = Katabasis::Activity::Idle; - setStage(AuthStage::Complete); - m_data->validity_ = m_data->minecraftProfile.validity; - emit activityChanged(m_activity); -} - -void AuthContext::initMSA() { - if(m_oauth2) { - return; - } - - OAuth2::Options opts; - opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = APPLICATION->msaClientId(); - opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; - opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; - - // FIXME: OAuth2 is not aware of our fancy shared pointers - m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); - - connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); - connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); -} - -void AuthContext::initMojang() { - if(m_yggdrasil) { - return; - } - m_yggdrasil = new Yggdrasil(m_data, this); - - connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed); - connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded); -} - -void AuthContext::onMojangSucceeded() { - doMinecraftProfile(); -} - - -void AuthContext::onMojangFailed() { - finishActivity(); - m_error = m_yggdrasil->m_error; - m_aborted = m_yggdrasil->m_aborted; - changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); -} - -void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { - switch(activity) { - case Katabasis::Activity::Idle: - case Katabasis::Activity::LoggingIn: - case Katabasis::Activity::Refreshing: - case Katabasis::Activity::LoggingOut: { - // We asked it to do something, it's doing it. Nothing to act upon. - return; - } - case Katabasis::Activity::Succeeded: { - // Succeeded or did not invalidate tokens - emit hideVerificationUriAndCode(); - if (!m_oauth2->linked()) { - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); - return; - } - QVariantMap extraTokens = m_oauth2->extraTokens(); -#ifndef NDEBUG - if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; - foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); - } - } -#endif - doUserAuth(); - return; - } - case Katabasis::Activity::FailedSoft: { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error.")); - return; - } - case Katabasis::Activity::FailedGone: - case Katabasis::Activity::FailedHard: { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); - return; - } - default: { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); - return; - } - - } -} - -void AuthContext::doUserAuth() { - setStage(AuthStage::UserAuth); - changeState(STATE_WORKING, tr("Starting user authentication")); - - QString xbox_auth_template = R"XXX( -{ - "Properties": { - "AuthMethod": "RPS", - "SiteName": "user.auth.xboxlive.com", - "RpsTicket": "d=%1" - }, - "RelyingParty": "http://auth.xboxlive.com", - "TokenType": "JWT" -} -)XXX"; - auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - auto *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onUserAuthDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "First layer of XBox auth ... commencing."; -} - -void AuthContext::onUserAuthDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList<QNetworkReply::RawHeaderPair> headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - finishActivity(); - changeState(STATE_FAILED_HARD, tr("XBox user authentication failed.")); - return; - } - - Katabasis::Token temp; - if(!Parsers::parseXTokenResponse(replyData, temp, "UToken")) { - qWarning() << "Could not parse user authentication response..."; - finishActivity(); - changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); - return; - } - m_data->userToken = temp; - - setStage(AuthStage::XboxAuth); - changeState(STATE_WORKING, tr("Starting XBox authentication")); - - doSTSAuthMinecraft(); - doSTSAuthGeneric(); -} -/* - url = "https://xsts.auth.xboxlive.com/xsts/authorize" - headers = {"x-xbl-contract-version": "1"} - data = { - "RelyingParty": relying_party, - "TokenType": "JWT", - "Properties": { - "UserTokens": [self.user_token.token], - "SandboxId": "RETAIL", - }, - } -*/ -void AuthContext::doSTSAuthMinecraft() { - QString xbox_auth_template = R"XXX( -{ - "Properties": { - "SandboxId": "RETAIL", - "UserTokens": [ - "%1" - ] - }, - "RelyingParty": "rp://api.minecraftservices.com/", - "TokenType": "JWT" -} -)XXX"; - auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onSTSAuthMinecraftDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Getting Minecraft services STS token..."; -} - -void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) { - if(error == QNetworkReply::AuthenticationRequiredError) { - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); - return; - } - - int64_t errorCode = -1; - auto obj = doc.object(); - if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { - qWarning() << "XErr is not a number"; - return; - } - stsErrors.insert(errorCode); - stsFailed = true; - } -} - - -void AuthContext::onSTSAuthMinecraftDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList<QNetworkReply::RawHeaderPair> headers -) { -#ifndef NDEBUG - qDebug() << replyData; -#endif - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - processSTSError(error, replyData, headers); - failResult(m_mcAuthSucceeded); - return; - } - - Katabasis::Token temp; - if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { - qWarning() << "Could not parse authorization response for access to mojang services..."; - failResult(m_mcAuthSucceeded); - return; - } - - if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { - qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; - failResult(m_mcAuthSucceeded); - return; - } - m_data->mojangservicesToken = temp; - - doMinecraftAuth(); -} - -void AuthContext::doMinecraftAuth() { - auto requestURL = "https://api.minecraftservices.com/launcher/login"; - auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); - auto xToken = m_data->mojangservicesToken.token; - - QString mc_auth_template = R"XXX( -{ - "xtoken": "XBL3.0 x=%1;%2", - "platform": "PC_LAUNCHER" -} -)XXX"; - auto requestBody = mc_auth_template.arg(uhs, xToken); - - QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftAuthDone); - requestor->post(request, requestBody.toUtf8()); - qDebug() << "Getting Minecraft access token..."; -} - -void AuthContext::onMinecraftAuthDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList<QNetworkReply::RawHeaderPair> headers -) { - qDebug() << replyData; - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << replyData; -#endif - failResult(m_mcAuthSucceeded); - return; - } - - if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) { - qWarning() << "Could not parse login_with_xbox response..."; -#ifndef NDEBUG - qDebug() << replyData; -#endif - failResult(m_mcAuthSucceeded); - return; - } - - succeedResult(m_mcAuthSucceeded); -} - -void AuthContext::doSTSAuthGeneric() { - QString xbox_auth_template = R"XXX( -{ - "Properties": { - "SandboxId": "RETAIL", - "UserTokens": [ - "%1" - ] - }, - "RelyingParty": "http://xboxlive.com", - "TokenType": "JWT" -} -)XXX"; - auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onSTSAuthGenericDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Getting generic STS token..."; -} - -void AuthContext::onSTSAuthGenericDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList<QNetworkReply::RawHeaderPair> headers -) { -#ifndef NDEBUG - qDebug() << replyData; -#endif - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - processSTSError(error, replyData, headers); - failResult(m_xboxProfileSucceeded); - return; - } - - Katabasis::Token temp; - if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { - qWarning() << "Could not parse authorization response for access to xbox API..."; - failResult(m_xboxProfileSucceeded); - return; - } - - if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { - qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; - failResult(m_xboxProfileSucceeded); - return; - } - m_data->xboxApiToken = temp; - - doXBoxProfile(); -} - -void AuthContext::doXBoxProfile() { - auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); - QUrlQuery q; - q.addQueryItem( - "settings", - "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," - "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," - "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," - "PreferredColor,Location,Bio,Watermarks," - "RealName,RealNameOverride,IsQuarantined" - ); - url.setQuery(q); - - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("x-xbl-contract-version", "3"); - request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onXBoxProfileDone); - requestor->get(request); - qDebug() << "Getting Xbox profile..."; -} - -void AuthContext::onXBoxProfileDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList<QNetworkReply::RawHeaderPair> headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << replyData; -#endif - failResult(m_xboxProfileSucceeded); - return; - } - -#ifndef NDEBUG - qDebug() << "XBox profile: " << replyData; -#endif - - succeedResult(m_xboxProfileSucceeded); -} - -void AuthContext::succeedResult(bool& flag) { - m_requestsDone ++; - flag = true; - checkResult(); -} - -void AuthContext::failResult(bool& flag) { - m_requestsDone ++; - flag = false; - checkResult(); -} - -void AuthContext::checkResult() { - qDebug() << "AuthContext::checkResult called"; - if(m_requestsDone != 2) { - qDebug() << "Number of ready results:" << m_requestsDone; - return; - } - if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { - doEntitlements(); - } - else { - finishActivity(); - if(stsFailed) { - if(stsErrors.contains(2148916233)) { - changeState( - STATE_FAILED_HARD, - tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") - .arg("<a href=\"https://www.minecraft.net/en-us/store/minecraft-java-edition\">minecraft.net</a>") - ); - } - else if (stsErrors.contains(2148916235)){ - // NOTE: this is the Grulovia error - changeState( - STATE_FAILED_HARD, - tr("XBox Live is not available in your country. You've been blocked.") - ); - } - else if (stsErrors.contains(2148916238)){ - changeState( - STATE_FAILED_HARD, - tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") - .arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>") - ); - } - else { - QStringList errorList; - for(auto & error: stsErrors) { - errorList.append(QString::number(error)); - } - changeState( - STATE_FAILED_HARD, - tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorList.join("\n")) - ); - } - } - else { - changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed")); - } - } -} - -void AuthContext::doEntitlements() { - auto uuid = QUuid::createUuid(); - entitlementsRequestId = uuid.toString().remove('{').remove('}'); - auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + entitlementsRequestId; - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onEntitlementsDone); - requestor->get(request); - qDebug() << "Getting Xbox profile..."; -} - - -void AuthContext::onEntitlementsDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList<QNetworkReply::RawHeaderPair> headers -) { -#ifndef NDEBUG - qDebug() << data; -#endif - // TODO: check presence of same entitlementsRequestId? - // TODO: validate JWTs? - Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); - doMinecraftProfile(); -} - -void AuthContext::doMinecraftProfile() { - setStage(AuthStage::MinecraftProfile); - changeState(STATE_WORKING, tr("Starting minecraft profile acquisition")); - - auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - // request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftProfileDone); - requestor->get(request); -} - -void AuthContext::onMinecraftProfileDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList<QNetworkReply::RawHeaderPair> headers -) { -#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) { - m_data->minecraftEntitlement.canPlayMinecraft = false; - m_data->minecraftEntitlement.ownsMinecraft = false; - } - m_data->minecraftProfile = MinecraftProfile(); - succeed(); - return; - } - if (error != QNetworkReply::NoError) { - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); - return; - } - if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { - m_data->minecraftProfile = MinecraftProfile(); - finishActivity(); - changeState(STATE_FAILED_HARD, 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; - m_data->minecraftEntitlement.canPlayMinecraft = validProfile; - m_data->minecraftEntitlement.ownsMinecraft = validProfile; - doMigrationEligibilityCheck(); - } - else { - doGetSkin(); - } -} - -void AuthContext::doMigrationEligibilityCheck() { - setStage(AuthStage::MigrationEligibility); - changeState(STATE_WORKING, tr("Starting check for migration eligibility")); - - auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onMigrationEligibilityCheckDone); - requestor->get(request); -} - -void AuthContext::onMigrationEligibilityCheckDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList<QNetworkReply::RawHeaderPair> headers -) { - if (error == QNetworkReply::NoError) { - Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); - } - doGetSkin(); -} - -void AuthContext::doGetSkin() { - setStage(AuthStage::Skin); - changeState(STATE_WORKING, tr("Fetching player skin")); - - auto url = QUrl(m_data->minecraftProfile.skin.url); - QNetworkRequest request = QNetworkRequest(url); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onSkinDone); - requestor->get(request); -} - -void AuthContext::onSkinDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList<QNetworkReply::RawHeaderPair> -) { - if (error == QNetworkReply::NoError) { - m_data->minecraftProfile.skin.data = data; - } - succeed(); - -} - -void AuthContext::succeed() { - m_data->validity_ = Katabasis::Validity::Certain; - finishActivity(); - changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); -} - -void AuthContext::setStage(AuthContext::AuthStage stage) { - m_stage = stage; - emit progress((int)m_stage, (int)AuthStage::Complete); -} - - -QString AuthContext::getStateMessage() const { - switch (m_accountState) - { - case STATE_WORKING: - switch(m_stage) { - case AuthStage::Initial: { - QString loginMessage = tr("Logging in as %1 user"); - if(m_data->type == AccountType::MSA) { - return loginMessage.arg("Microsoft"); - } - else { - return loginMessage.arg("Mojang"); - } - } - case AuthStage::UserAuth: - return tr("Logging in as XBox user"); - case AuthStage::XboxAuth: - return tr("Logging in with XBox and Mojang services"); - case AuthStage::MinecraftProfile: - return tr("Getting Minecraft profile"); - case AuthStage::MigrationEligibility: - return tr("Checking for migration eligibility"); - case AuthStage::Skin: - return tr("Getting Minecraft skin"); - case AuthStage::Complete: - return tr("Finished"); - default: - break; - } - default: - return AccountTask::getStateMessage(); - } -} diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h deleted file mode 100644 index 5e4e9edc..00000000 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include <QObject> -#include <QList> -#include <QVector> -#include <QSet> -#include <QNetworkReply> -#include <QImage> - -#include <katabasis/DeviceFlow.h> -#include "Yggdrasil.h" -#include "../AccountData.h" -#include "../AccountTask.h" - -class AuthContext : public AccountTask -{ - Q_OBJECT - -public: - explicit AuthContext(AccountData * data, QObject *parent = 0); - - bool isBusy() { - return m_activity != Katabasis::Activity::Idle; - }; - Katabasis::Validity validity() { - return m_data->validity_; - }; - - //bool signOut(); - - QString getStateMessage() const override; - -signals: - void activityChanged(Katabasis::Activity activity); - -private slots: -// OAuth-specific callbacks - void onOAuthActivityChanged(Katabasis::Activity activity); - -// Yggdrasil specific callbacks - void onMojangSucceeded(); - void onMojangFailed(); - -protected: - void initMSA(); - void initMojang(); - - void doUserAuth(); - Q_SLOT void onUserAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void processSTSError(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doSTSAuthMinecraft(); - Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - void doMinecraftAuth(); - Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doSTSAuthGeneric(); - Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - void doXBoxProfile(); - Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doEntitlements(); - Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doMinecraftProfile(); - Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doMigrationEligibilityCheck(); - Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doGetSkin(); - Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void succeed(); - - void failResult(bool & flag); - void succeedResult(bool & flag); - void checkResult(); - -protected: - void beginActivity(Katabasis::Activity activity); - void finishActivity(); - void clearTokens(); - -protected: - Katabasis::DeviceFlow *m_oauth2 = nullptr; - Yggdrasil *m_yggdrasil = nullptr; - - int m_requestsDone = 0; - bool m_xboxProfileSucceeded = false; - bool m_mcAuthSucceeded = false; - QString entitlementsRequestId; - - QSet<int64_t> stsErrors; - bool stsFailed = false; - - Katabasis::Activity m_activity = Katabasis::Activity::Idle; - enum class AuthStage { - Initial, - UserAuth, - XboxAuth, - MinecraftProfile, - MigrationEligibility, - Skin, - Complete - } m_stage = AuthStage::Initial; - - void setStage(AuthStage stage); -}; diff --git a/launcher/minecraft/auth/flows/AuthFlow.cpp b/launcher/minecraft/auth/flows/AuthFlow.cpp new file mode 100644 index 00000000..4f78e8c3 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthFlow.cpp @@ -0,0 +1,71 @@ +#include <QNetworkAccessManager> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QDebug> + +#include "AuthFlow.h" +#include "katabasis/Globals.h" + +#include <Application.h> + +AuthFlow::AuthFlow(AccountData * data, QObject *parent) : + AccountTask(data, parent) +{ +} + +void AuthFlow::succeed() { + m_data->validity_ = Katabasis::Validity::Certain; + changeState( + AccountTaskState::STATE_SUCCEEDED, + tr("Finished all authentication steps") + ); +} + +void AuthFlow::executeTask() { + if(m_currentStep) { + return; + } + changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); + nextStep(); +} + +void AuthFlow::nextStep() { + if(m_steps.size() == 0) { + // we got to the end without an incident... assume this is all. + m_currentStep.reset(); + succeed(); + return; + } + m_currentStep = m_steps.front(); + qDebug() << "AuthFlow:" << m_currentStep->describe(); + m_steps.pop_front(); + connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); + connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode); + connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode); + + m_currentStep->perform(); +} + + +QString AuthFlow::getStateMessage() const { + switch (m_taskState) + { + case AccountTaskState::STATE_WORKING: { + if(m_currentStep) { + return m_currentStep->describe(); + } + else { + return tr("Working..."); + } + } + default: { + return AccountTask::getStateMessage(); + } + } +} + +void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) { + if(changeState(resultingState, message)) { + nextStep(); + } +} diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h new file mode 100644 index 00000000..e067cc99 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthFlow.h @@ -0,0 +1,45 @@ +#pragma once + +#include <QObject> +#include <QList> +#include <QVector> +#include <QSet> +#include <QNetworkReply> +#include <QImage> + +#include <katabasis/DeviceFlow.h> + +#include "minecraft/auth/Yggdrasil.h" +#include "minecraft/auth/AccountData.h" +#include "minecraft/auth/AccountTask.h" +#include "minecraft/auth/AuthStep.h" + +class AuthFlow : public AccountTask +{ + Q_OBJECT + +public: + explicit AuthFlow(AccountData * data, QObject *parent = 0); + + Katabasis::Validity validity() { + return m_data->validity_; + }; + + QString getStateMessage() const override; + + void executeTask() override; + +signals: + void activityChanged(Katabasis::Activity activity); + +private slots: + void stepFinished(AccountTaskState resultingState, QString message); + +protected: + void succeed(); + void nextStep(); + +protected: + QList<AuthStep::Ptr> m_steps; + AuthStep::Ptr m_currentStep; +}; diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp new file mode 100644 index 00000000..416b8f2c --- /dev/null +++ b/launcher/minecraft/auth/flows/MSA.cpp @@ -0,0 +1,37 @@ +#include "MSA.h" + +#include "minecraft/auth/steps/MSAStep.h" +#include "minecraft/auth/steps/XboxUserStep.h" +#include "minecraft/auth/steps/XboxAuthorizationStep.h" +#include "minecraft/auth/steps/LauncherLoginStep.h" +#include "minecraft/auth/steps/XboxProfileStep.h" +#include "minecraft/auth/steps/EntitlementsStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) { + m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh)); + m_steps.append(new XboxUserStep(m_data)); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(new LauncherLoginStep(m_data)); + m_steps.append(new XboxProfileStep(m_data)); + m_steps.append(new EntitlementsStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +MSAInteractive::MSAInteractive( + AccountData* data, + QObject* parent +) : AuthFlow(data, parent) { + m_steps.append(new MSAStep(m_data, MSAStep::Action::Login)); + m_steps.append(new XboxUserStep(m_data)); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(new LauncherLoginStep(m_data)); + m_steps.append(new XboxProfileStep(m_data)); + m_steps.append(new EntitlementsStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/MSA.h b/launcher/minecraft/auth/flows/MSA.h new file mode 100644 index 00000000..14a4ff43 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSA.h @@ -0,0 +1,22 @@ +#pragma once +#include "AuthFlow.h" + +class MSAInteractive : public AuthFlow +{ + Q_OBJECT +public: + explicit MSAInteractive( + AccountData *data, + QObject *parent = 0 + ); +}; + +class MSASilent : public AuthFlow +{ + Q_OBJECT +public: + explicit MSASilent( + AccountData * data, + QObject *parent = 0 + ); +}; diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp deleted file mode 100644 index 525aaf88..00000000 --- a/launcher/minecraft/auth/flows/MSAInteractive.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "MSAInteractive.h" - -MSAInteractive::MSAInteractive( - AccountData* data, - QObject* parent -) : AuthContext(data, parent) {} - -void MSAInteractive::executeTask() { - m_requestsDone = 0; - m_xboxProfileSucceeded = false; - m_mcAuthSucceeded = false; - - initMSA(); - - QVariantMap extraOpts; - extraOpts["prompt"] = "select_account"; - m_oauth2->setExtraRequestParams(extraOpts); - - beginActivity(Katabasis::Activity::LoggingIn); - *m_data = AccountData(); - m_oauth2->login(); -} diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h deleted file mode 100644 index 6654e0d6..00000000 --- a/launcher/minecraft/auth/flows/MSAInteractive.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "AuthContext.h" - -class MSAInteractive : public AuthContext -{ - Q_OBJECT -public: - explicit MSAInteractive( - AccountData *data, - QObject *parent = 0 - ); - void executeTask() override; -}; diff --git a/launcher/minecraft/auth/flows/MSASilent.cpp b/launcher/minecraft/auth/flows/MSASilent.cpp deleted file mode 100644 index 8ce43c1f..00000000 --- a/launcher/minecraft/auth/flows/MSASilent.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "MSASilent.h" - -MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {} - -void MSASilent::executeTask() { - m_requestsDone = 0; - m_xboxProfileSucceeded = false; - m_mcAuthSucceeded = false; - - initMSA(); - - beginActivity(Katabasis::Activity::Refreshing); - if(!m_oauth2->refresh()) { - finishActivity(); - } -} diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h deleted file mode 100644 index a442b49e..00000000 --- a/launcher/minecraft/auth/flows/MSASilent.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "AuthContext.h" - -class MSASilent : public AuthContext -{ - Q_OBJECT -public: - explicit MSASilent( - AccountData * data, - QObject *parent = 0 - ); - void executeTask() override; -}; diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp new file mode 100644 index 00000000..4661dbe2 --- /dev/null +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -0,0 +1,27 @@ +#include "Mojang.h" + +#include "minecraft/auth/steps/YggdrasilStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/MigrationEligibilityStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +MojangRefresh::MojangRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new YggdrasilStep(m_data, QString())); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MigrationEligibilityStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +MojangLogin::MojangLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new YggdrasilStep(m_data, m_password)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MigrationEligibilityStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Mojang.h b/launcher/minecraft/auth/flows/Mojang.h new file mode 100644 index 00000000..c09c81a8 --- /dev/null +++ b/launcher/minecraft/auth/flows/Mojang.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class MojangRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit MojangRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class MojangLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit MojangLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp deleted file mode 100644 index 6c217cd1..00000000 --- a/launcher/minecraft/auth/flows/MojangLogin.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "MojangLogin.h" - -MojangLogin::MojangLogin( - AccountData *data, - QString password, - QObject *parent -): AuthContext(data, parent), m_password(password) {} - -void MojangLogin::executeTask() { - m_requestsDone = 0; - m_xboxProfileSucceeded = false; - m_mcAuthSucceeded = false; - - initMojang(); - - beginActivity(Katabasis::Activity::LoggingIn); - m_yggdrasil->login(m_password); -} diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h deleted file mode 100644 index 5f33752f..00000000 --- a/launcher/minecraft/auth/flows/MojangLogin.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "AuthContext.h" - -class MojangLogin : public AuthContext -{ - Q_OBJECT -public: - explicit MojangLogin( - AccountData *data, - QString password, - QObject *parent = 0 - ); - void executeTask() override; - -private: - QString m_password; -}; diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp deleted file mode 100644 index 008c0453..00000000 --- a/launcher/minecraft/auth/flows/MojangRefresh.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "MojangRefresh.h" - -MojangRefresh::MojangRefresh( - AccountData *data, - QObject *parent -) : AuthContext(data, parent) {} - -void MojangRefresh::executeTask() { - m_requestsDone = 0; - m_xboxProfileSucceeded = false; - m_mcAuthSucceeded = false; - - initMojang(); - - beginActivity(Katabasis::Activity::Refreshing); - m_yggdrasil->refresh(); -} diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h deleted file mode 100644 index 06e4e4ce..00000000 --- a/launcher/minecraft/auth/flows/MojangRefresh.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once -#include "AuthContext.h" - -class MojangRefresh : public AuthContext -{ - Q_OBJECT -public: - explicit MojangRefresh(AccountData *data, QObject *parent = 0); - void executeTask() override; -}; diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp new file mode 100644 index 00000000..f726244f --- /dev/null +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -0,0 +1,53 @@ +#include "EntitlementsStep.h" + +#include <QNetworkRequest> +#include <QUuid> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} + +EntitlementsStep::~EntitlementsStep() noexcept = default; + +QString EntitlementsStep::describe() { + return tr("Determining game ownership."); +} + + +void EntitlementsStep::perform() { + auto uuid = QUuid::createUuid(); + m_entitlementsRequestId = uuid.toString().remove('{').remove('}'); + auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId; + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone); + requestor->get(request); + qDebug() << "Getting entitlements..."; +} + +void EntitlementsStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void EntitlementsStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + + // TODO: check presence of same entitlementsRequestId? + // TODO: validate JWTs? + Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + + emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); +} diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h new file mode 100644 index 00000000..9412ae79 --- /dev/null +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -0,0 +1,25 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class EntitlementsStep : public AuthStep { + Q_OBJECT + +public: + explicit EntitlementsStep(AccountData *data); + virtual ~EntitlementsStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + +private: + QString m_entitlementsRequestId; +}; diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp new file mode 100644 index 00000000..3521f8dc --- /dev/null +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -0,0 +1,43 @@ + +#include "GetSkinStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) { + +} + +GetSkinStep::~GetSkinStep() noexcept = default; + +QString GetSkinStep::describe() { + return tr("Getting skin."); +} + +void GetSkinStep::perform() { + auto url = QUrl(m_data->minecraftProfile.skin.url); + QNetworkRequest request = QNetworkRequest(url); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone); + requestor->get(request); +} + +void GetSkinStep::rehydrate() { + // NOOP, for now. +} + +void GetSkinStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + m_data->minecraftProfile.skin.data = data; + } + emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); +} diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h new file mode 100644 index 00000000..6b97371e --- /dev/null +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class GetSkinStep : public AuthStep { + Q_OBJECT + +public: + explicit GetSkinStep(AccountData *data); + virtual ~GetSkinStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp new file mode 100644 index 00000000..c978bd07 --- /dev/null +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -0,0 +1,78 @@ +#include "LauncherLoginStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/AccountTask.h" + +LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { + +} + +LauncherLoginStep::~LauncherLoginStep() noexcept = default; + +QString LauncherLoginStep::describe() { + return tr("Accessing Mojang services."); +} + +void LauncherLoginStep::perform() { + auto requestURL = "https://api.minecraftservices.com/launcher/login"; + auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); + auto xToken = m_data->mojangservicesToken.token; + + QString mc_auth_template = R"XXX( +{ + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" +} +)XXX"; + auto requestBody = mc_auth_template.arg(uhs, xToken); + + QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone); + requestor->post(request, requestBody.toUtf8()); + qDebug() << "Getting Minecraft access token..."; +} + +void LauncherLoginStep::rehydrate() { + // TODO: check the token validity +} + +void LauncherLoginStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + + qDebug() << data; + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << data; +#endif + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_) + ); + return; + } + + if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { + qWarning() << "Could not parse login_with_xbox response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to parse the Minecraft access token response.") + ); + return; + } + emit finished(AccountTaskState::STATE_WORKING, tr("")); +} diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h new file mode 100644 index 00000000..e06a306f --- /dev/null +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class LauncherLoginStep : public AuthStep { + Q_OBJECT + +public: + explicit LauncherLoginStep(AccountData *data); + virtual ~LauncherLoginStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp new file mode 100644 index 00000000..be711f7e --- /dev/null +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -0,0 +1,111 @@ +#include "MSAStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +#include "Application.h" + +using OAuth2 = Katabasis::DeviceFlow; +using Activity = Katabasis::Activity; + +MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) { + OAuth2::Options opts; + opts.scope = "XboxLive.signin offline_access"; + opts.clientIdentifier = APPLICATION->msaClientId(); + opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + // FIXME: OAuth2 is not aware of our fancy shared pointers + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); + + connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); +} + +MSAStep::~MSAStep() noexcept = default; + +QString MSAStep::describe() { + return tr("Logging in with Microsoft account."); +} + + +void MSAStep::rehydrate() { + switch(m_action) { + case Refresh: { + // TODO: check the tokens and see if they are old (older than a day) + return; + } + case Login: { + // NOOP + return; + } + } +} + +void MSAStep::perform() { + switch(m_action) { + case Refresh: { + m_oauth2->refresh(); + return; + } + case Login: { + QVariantMap extraOpts; + extraOpts["prompt"] = "select_account"; + m_oauth2->setExtraRequestParams(extraOpts); + + *m_data = AccountData(); + m_oauth2->login(); + return; + } + } +} + +void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) { + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::Refreshing: + case Katabasis::Activity::LoggingOut: { + // We asked it to do something, it's doing it. Nothing to act upon. + return; + } + case Katabasis::Activity::Succeeded: { + // Succeeded or did not invalidate tokens + emit hideVerificationUriAndCode(); + QVariantMap extraTokens = m_oauth2->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } +#endif + emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + return; + } + case Katabasis::Activity::FailedSoft: { + // NOTE: soft error in the first step means 'offline' + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error.")); + return; + } + case Katabasis::Activity::FailedGone: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists.")); + return; + } + case Katabasis::Activity::FailedHard: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); + return; + } + default: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); + return; + } + } +} diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h new file mode 100644 index 00000000..49ba3542 --- /dev/null +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -0,0 +1,32 @@ + +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include <katabasis/DeviceFlow.h> + +class MSAStep : public AuthStep { + Q_OBJECT +public: + enum Action { + Refresh, + Login + }; +public: + explicit MSAStep(AccountData *data, Action action); + virtual ~MSAStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onOAuthActivityChanged(Katabasis::Activity activity); + +private: + Katabasis::DeviceFlow *m_oauth2 = nullptr; + Action m_action; +}; diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp new file mode 100644 index 00000000..f5b5637a --- /dev/null +++ b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp @@ -0,0 +1,45 @@ +#include "MigrationEligibilityStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) { + +} + +MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default; + +QString MigrationEligibilityStep::describe() { + return tr("Checking for migration eligibility."); +} + +void MigrationEligibilityStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &MigrationEligibilityStep::onRequestDone); + requestor->get(request); +} + +void MigrationEligibilityStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MigrationEligibilityStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); + } + emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags")); +} diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.h b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h new file mode 100644 index 00000000..b1bf9cbf --- /dev/null +++ b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MigrationEligibilityStep : public AuthStep { + Q_OBJECT + +public: + explicit MigrationEligibilityStep(AccountData *data); + virtual ~MigrationEligibilityStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp new file mode 100644 index 00000000..9fef99b0 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -0,0 +1,83 @@ +#include "MinecraftProfileStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) { + +} + +MinecraftProfileStep::~MinecraftProfileStep() noexcept = default; + +QString MinecraftProfileStep::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void MinecraftProfileStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone); + requestor->get(request); +} + +void MinecraftProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MinecraftProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(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. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + 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; + 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/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h new file mode 100644 index 00000000..8ef3395c --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MinecraftProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit MinecraftProfileStep(AccountData *data); + virtual ~MinecraftProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp new file mode 100644 index 00000000..07eeb7dc --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -0,0 +1,158 @@ +#include "XboxAuthorizationStep.h" + +#include <QNetworkRequest> +#include <QJsonParseError> +#include <QJsonDocument> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind): + AuthStep(data), + m_token(token), + m_relyingParty(relyingParty), + m_authorizationKind(authorizationKind) +{ +} + +XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default; + +QString XboxAuthorizationStep::describe() { + return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); +} + +void XboxAuthorizationStep::rehydrate() { + // FIXME: check if the tokens are good? +} + +void XboxAuthorizationStep::perform() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + "%1" + ] + }, + "RelyingParty": "%2", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); +// http://xboxlive.com + QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "Getting authorization token for " << m_relyingParty; +} + +void XboxAuthorizationStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + if(!processSTSError(error, data, headers)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get authorization for %1 services. Error %1.").arg(m_authorizationKind, error) + ); + } + return; + } + + Katabasis::Token temp; + if(!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind) + ); + return; + } + + if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Server has changed %1 authorization user hash in the reply. Something is wrong.").arg(m_authorizationKind) + ); + return; + } + auto & token = *m_token; + token = temp; + + emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); +} + + +bool XboxAuthorizationStep::processSTSError( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + if(error == QNetworkReply::AuthenticationRequiredError) { + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString()) + ); + return true; + } + + int64_t errorCode = -1; + auto obj = doc.object(); + if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XErr element is missing from %1 authorization error response.").arg(m_authorizationKind) + ); + return true; + } + switch(errorCode) { + case 2148916233:{ + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") + .arg("<a href=\"https://www.minecraft.net/en-us/store/minecraft-java-edition\">minecraft.net</a>") + ); + return true; + } + case 2148916235: { + // NOTE: this is the Grulovia error + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XBox Live is not available in your country. You've been blocked.") + ); + return true; + } + case 2148916238: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") + .arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>") + ); + return true; + } + default: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorCode) + ); + return true; + } + } + } + return false; +} diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h new file mode 100644 index 00000000..31e43bf0 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -0,0 +1,34 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxAuthorizationStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxAuthorizationStep(AccountData *data, Katabasis::Token *token, QString relyingParty, QString authorizationKind); + virtual ~XboxAuthorizationStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private: + bool processSTSError( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers + ); + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + +private: + Katabasis::Token *m_token; + QString m_relyingParty; + QString m_authorizationKind; +}; diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp new file mode 100644 index 00000000..9f50138e --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -0,0 +1,73 @@ +#include "XboxProfileStep.h" + +#include <QNetworkRequest> +#include <QUrlQuery> + + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) { + +} + +XboxProfileStep::~XboxProfileStep() noexcept = default; + +QString XboxProfileStep::describe() { + return tr("Fetching Xbox profile."); +} + +void XboxProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void XboxProfileStep::perform() { + auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); + QUrlQuery q; + q.addQueryItem( + "settings", + "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," + "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," + "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined" + ); + url.setQuery(q); + + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("x-xbl-contract-version", "3"); + request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone); + requestor->get(request); + qDebug() << "Getting Xbox profile..."; +} + +void XboxProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << data; +#endif + finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to retrieve the Xbox profile.") + ); + return; + } + +#ifndef NDEBUG + qDebug() << "XBox profile: " << data; +#endif + + emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); +} diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.h b/launcher/minecraft/auth/steps/XboxProfileStep.h new file mode 100644 index 00000000..7a0c5873 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxProfileStep(AccountData *data); + virtual ~XboxProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp new file mode 100644 index 00000000..a38a28e4 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -0,0 +1,68 @@ +#include "XboxUserStep.h" + +#include <QNetworkRequest> + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) { + +} + +XboxUserStep::~XboxUserStep() noexcept = default; + +QString XboxUserStep::describe() { + return tr("Logging in as an Xbox user."); +} + + +void XboxUserStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void XboxUserStep::perform() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": "d=%1" + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); + + QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + auto *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "First layer of XBox auth ... commencing."; +} + +void XboxUserStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed.")); + return; + } + + Katabasis::Token temp; + if(!Parsers::parseXTokenResponse(data, temp, "UToken")) { + qWarning() << "Could not parse user authentication response..."; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood.")); + return; + } + m_data->userToken = temp; + emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token")); +} diff --git a/launcher/minecraft/auth/steps/XboxUserStep.h b/launcher/minecraft/auth/steps/XboxUserStep.h new file mode 100644 index 00000000..83e9405f --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxUserStep.h @@ -0,0 +1,22 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxUserStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxUserStep(AccountData *data); + virtual ~XboxUserStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); +}; diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp new file mode 100644 index 00000000..4c6b1624 --- /dev/null +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -0,0 +1,51 @@ +#include "YggdrasilStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded); +} + +YggdrasilStep::~YggdrasilStep() noexcept = default; + +QString YggdrasilStep::describe() { + return tr("Logging in with Mojang account."); +} + +void YggdrasilStep::rehydrate() { + // NOOP, for now. +} + +void YggdrasilStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password); + } + else { + m_yggdrasil->refresh(); + } +} + +void YggdrasilStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); +} + +void YggdrasilStep::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("Mojang user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Mojang user authentication ended with a network error."); + } + emit finished(state, errorMessage); +} diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.h b/launcher/minecraft/auth/steps/YggdrasilStep.h new file mode 100644 index 00000000..ebafb8e5 --- /dev/null +++ b/launcher/minecraft/auth/steps/YggdrasilStep.h @@ -0,0 +1,28 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class YggdrasilStep : public AuthStep { + Q_OBJECT + +public: + explicit YggdrasilStep(AccountData *data, QString password); + virtual ~YggdrasilStep() 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/legacy/LegacyInstance.cpp b/launcher/minecraft/legacy/LegacyInstance.cpp index c2b4309c..f467ec06 100644 --- a/launcher/minecraft/legacy/LegacyInstance.cpp +++ b/launcher/minecraft/legacy/LegacyInstance.cpp @@ -122,6 +122,11 @@ QString LegacyInstance::binRoot() const return FS::PathCombine(gameRoot(), "bin"); } +QString LegacyInstance::modsRoot() const { + return FS::PathCombine(gameRoot(), "mods"); +} + + QString LegacyInstance::jarModsDir() const { return FS::PathCombine(instanceRoot(), "instMods"); @@ -137,11 +142,6 @@ QString LegacyInstance::savesDir() const return FS::PathCombine(gameRoot(), "saves"); } -QString LegacyInstance::loaderModsDir() const -{ - return FS::PathCombine(gameRoot(), "mods"); -} - QString LegacyInstance::coreModsDir() const { return FS::PathCombine(gameRoot(), "coremods"); diff --git a/launcher/minecraft/legacy/LegacyInstance.h b/launcher/minecraft/legacy/LegacyInstance.h index c6680fd0..298543f7 100644 --- a/launcher/minecraft/legacy/LegacyInstance.h +++ b/launcher/minecraft/legacy/LegacyInstance.h @@ -45,11 +45,13 @@ public: QString savesDir() const; QString texturePacksDir() const; QString jarModsDir() const; - QString loaderModsDir() const; QString coreModsDir() const; QString resourceDir() const; - virtual QString instanceConfigFolder() const override; + + QString instanceConfigFolder() const override; + QString gameRoot() const override; // Path to the instance's minecraft directory. + QString modsRoot() const override; // Path to the instance's minecraft directory. QString binRoot() const; // Path to the instance's minecraft bin directory. /// Get the curent base jar of this instance. By default, it's the diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index d411965a..e49c166a 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -5,15 +5,15 @@ #include "Application.h" -CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) - : Task(parent), m_capeId(cape), m_session(session) +CapeChange::CapeChange(QObject *parent, QString token, QString cape) + : Task(parent), m_capeId(cape), m_token(token) { } 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()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8()); setStatus(tr("Equipping cape")); @@ -27,7 +27,7 @@ void CapeChange::setCape(QString& cape) { 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()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QNetworkReply *rep = APPLICATION->network()->deleteResource(request); setStatus(tr("Removing cape")); diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h index c04ad8c7..185d69b6 100644 --- a/launcher/minecraft/services/CapeChange.h +++ b/launcher/minecraft/services/CapeChange.h @@ -3,7 +3,6 @@ #include <QFile> #include <QtNetwork/QtNetwork> #include <memory> -#include <minecraft/auth/AuthSession.h> #include "tasks/Task.h" #include "QObjectPtr.h" @@ -11,7 +10,7 @@ class CapeChange : public Task { Q_OBJECT public: - CapeChange(QObject *parent, AuthSessionPtr session, QString capeId); + CapeChange(QObject *parent, QString token, QString capeId); virtual ~CapeChange() {} private: @@ -20,7 +19,7 @@ private: private: QString m_capeId; - AuthSessionPtr m_session; + QString m_token; shared_qobject_ptr<QNetworkReply> m_reply; protected: diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp index a0b0330c..cce8364e 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/services/SkinDelete.cpp @@ -5,15 +5,15 @@ #include "Application.h" -SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session) - : Task(parent), m_session(session) +SkinDelete::SkinDelete(QObject *parent, QString token) + : Task(parent), m_token(token) { } void SkinDelete::executeTask() { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QNetworkReply *rep = APPLICATION->network()->deleteResource(request); m_reply = shared_qobject_ptr<QNetworkReply>(rep); diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h index 6048b33a..83a84685 100644 --- a/launcher/minecraft/services/SkinDelete.h +++ b/launcher/minecraft/services/SkinDelete.h @@ -2,7 +2,6 @@ #include <QFile> #include <QtNetwork/QtNetwork> -#include <minecraft/auth/AuthSession.h> #include "tasks/Task.h" typedef shared_qobject_ptr<class SkinDelete> SkinDeletePtr; @@ -11,11 +10,11 @@ class SkinDelete : public Task { Q_OBJECT public: - SkinDelete(QObject *parent, AuthSessionPtr session); + SkinDelete(QObject *parent, QString token); virtual ~SkinDelete() = default; private: - AuthSessionPtr m_session; + QString m_token; shared_qobject_ptr<QNetworkReply> m_reply; protected: @@ -25,4 +24,3 @@ public slots: void downloadError(QNetworkReply::NetworkError); void downloadFinished(); }; - diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index e58d32d7..7c2e8337 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -16,15 +16,15 @@ QByteArray getVariant(SkinUpload::Model model) { } } -SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, SkinUpload::Model model) - : Task(parent), m_model(model), m_skin(skin), m_session(session) +SkinUpload::SkinUpload(QObject *parent, QString token, QByteArray skin, SkinUpload::Model model) + : Task(parent), m_model(model), m_skin(skin), m_token(token) { } void SkinUpload::executeTask() { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart skin; diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h index 2c782e11..2c1f0a2e 100644 --- a/launcher/minecraft/services/SkinUpload.h +++ b/launcher/minecraft/services/SkinUpload.h @@ -3,7 +3,6 @@ #include <QFile> #include <QtNetwork/QtNetwork> #include <memory> -#include <minecraft/auth/AuthSession.h> #include "tasks/Task.h" typedef shared_qobject_ptr<class SkinUpload> SkinUploadPtr; @@ -19,13 +18,13 @@ public: }; // Note this class takes ownership of the file. - SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, Model model = STEVE); + SkinUpload(QObject *parent, QString token, QByteArray skin, Model model = STEVE); virtual ~SkinUpload() {} private: Model m_model; QByteArray m_skin; - AuthSessionPtr m_session; + QString m_token; shared_qobject_ptr<QNetworkReply> m_reply; protected: virtual void executeTask(); diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp index 35f50b18..e649c43a 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.cpp +++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@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 "ATLPackIndex.h" #include <QRegularExpression> diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h index 405a3448..337b80b8 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.h +++ b/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "ATLPackManifest.h" diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 9ef32db1..7352d174 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@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 "ATLPackInstallTask.h" #include <QtConcurrent/QtConcurrent> diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index f8ea2d54..783ec19b 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@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. + */ + #pragma once #include <meta/VersionList.h> diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index e25d8346..40be6d53 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Petr Mrazek <peterix@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 "ATLPackManifest.h" #include "Json.h" diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index ead216a5..673f2f8b 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -1,3 +1,19 @@ +/* + * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include <QString> diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 563b5cfa..5fd85dfc 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@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 "FTBPackInstallTask.h" #include "FileSystem.h" diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h index 23362dc9..ff59b695 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@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. + */ + #pragma once #include "FTBPackManifest.h" diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp index fd99d332..e2d47a5b 100644 --- a/launcher/modplatform/modpacksch/FTBPackManifest.cpp +++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@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 "FTBPackManifest.h" #include "Json.h" diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h index 7818b36d..da45d8ac 100644 --- a/launcher/modplatform/modpacksch/FTBPackManifest.h +++ b/launcher/modplatform/modpacksch/FTBPackManifest.h @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020 Petr Mrazek <peterix@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. + */ + #pragma once #include <QString> diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 83505635..52921512 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -3,5 +3,6 @@ <qresource prefix="/backgrounds"> <file alias="kitteh">catbgrnd2.png</file> <file alias="catmas">catmas.png</file> + <file alias="cattiversary">cattiversary.png</file> </qresource> </RCC> diff --git a/launcher/resources/backgrounds/cattiversary.png b/launcher/resources/backgrounds/cattiversary.png Binary files differnew file mode 100644 index 00000000..09a36566 --- /dev/null +++ b/launcher/resources/backgrounds/cattiversary.png diff --git a/launcher/resources/sources/burfcat_hat.png b/launcher/resources/sources/burfcat_hat.png Binary files differnew file mode 100644 index 00000000..a378c1fb --- /dev/null +++ b/launcher/resources/sources/burfcat_hat.png diff --git a/launcher/resources/sources/cattiversary.xcf b/launcher/resources/sources/cattiversary.xcf Binary files differnew file mode 100644 index 00000000..0026cd35 --- /dev/null +++ b/launcher/resources/sources/cattiversary.xcf diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index ce53ac32..576258eb 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -39,6 +39,20 @@ struct Language updated = (key == defaultLangCode); } + QString languageName() const { + QString result; + if(key == "ja_KANJI") { + result = locale.nativeLanguageName() + u8" (漢字)"; + } + else if(key == "es_UY") { + result = u8"español de Latinoamérica"; + } + else { + result = locale.nativeLanguageName(); + } + return result; + } + float percentTranslated() const { if (total == 0) @@ -340,7 +354,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const { case Column::Language: { - return lang.locale.nativeLanguageName(); + return lang.languageName(); } case Column::Completeness: { diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 95d9ae5d..b06f3d5a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -211,8 +211,10 @@ public: TranslatedAction actionEditInstNotes; TranslatedAction actionEditInstance; TranslatedAction actionWorlds; + TranslatedAction actionMods; TranslatedAction actionViewSelectedInstFolder; TranslatedAction actionViewSelectedMCFolder; + TranslatedAction actionViewSelectedModsFolder; TranslatedAction actionDeleteInstance; TranslatedAction actionConfig_Folder; TranslatedAction actionCAT; @@ -530,6 +532,13 @@ public: all_actions.append(&actionEditInstNotes); instanceToolBar->addAction(actionEditInstNotes); + actionMods = TranslatedAction(MainWindow); + actionMods->setObjectName(QStringLiteral("actionMods")); + actionMods.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Mods")); + actionMods.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the mods of this instance.")); + all_actions.append(&actionMods); + instanceToolBar->addAction(actionMods); + actionWorlds = TranslatedAction(MainWindow); actionWorlds->setObjectName(QStringLiteral("actionWorlds")); actionWorlds.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Worlds")); @@ -560,6 +569,15 @@ public: all_actions.append(&actionViewSelectedMCFolder); instanceToolBar->addAction(actionViewSelectedMCFolder); + /* + actionViewSelectedModsFolder = TranslatedAction(MainWindow); + actionViewSelectedModsFolder->setObjectName(QStringLiteral("actionViewSelectedModsFolder")); + actionViewSelectedModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Mods Folder")); + actionViewSelectedModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's mods folder in a file browser.")); + all_actions.append(&actionViewSelectedModsFolder); + instanceToolBar->addAction(actionViewSelectedModsFolder); + */ + actionConfig_Folder = TranslatedAction(MainWindow); actionConfig_Folder->setObjectName(QStringLiteral("actionConfig_Folder")); actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Config Folder")); @@ -1322,8 +1340,18 @@ void MainWindow::setCatBackground(bool enabled) if (enabled) { QDateTime now = QDateTime::currentDateTime(); + QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - QString cat = (non_stupid_abs(now.daysTo(xmas)) <= 4) ? "catmas" : "kitteh"; + QString cat; + if(non_stupid_abs(now.daysTo(xmas)) <= 4) { + cat = "catmas"; + } + else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { + cat = "cattiversary"; + } + else { + cat = "kitteh"; + } view->setStyleSheet(QString(R"( InstanceView { @@ -1641,6 +1669,11 @@ void MainWindow::on_actionWorlds_triggered() APPLICATION->showInstanceWindow(m_selectedInstance, "worlds"); } +void MainWindow::on_actionMods_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance, "mods"); +} + void MainWindow::on_actionEditInstance_triggered() { APPLICATION->showInstanceWindow(m_selectedInstance); @@ -1751,6 +1784,19 @@ void MainWindow::on_actionViewSelectedMCFolder_triggered() } } +void MainWindow::on_actionViewSelectedModsFolder_triggered() +{ + if (m_selectedInstance) + { + QString str = m_selectedInstance->modsRoot(); + if (!FS::ensureFilePathExists(str)) + { + // TODO: report error + return; + } + DesktopServices::openDirectory(QDir(str).absolutePath()); + } +} void MainWindow::closeEvent(QCloseEvent *event) { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 7e1256cc..e462c524 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -93,6 +93,8 @@ private slots: void on_actionViewSelectedMCFolder_triggered(); + void on_actionViewSelectedModsFolder_triggered(); + void refreshInstances(); void on_actionViewCentralModsFolder_triggered(); @@ -133,6 +135,8 @@ private slots: void on_actionEditInstNotes_triggered(); + void on_actionMods_triggered(); + void on_actionWorlds_triggered(); void on_actionScreenshots_triggered(); diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp index bf0806e1..194315a7 100644 --- a/launcher/ui/dialogs/LoginDialog.cpp +++ b/launcher/ui/dialogs/LoginDialog.cpp @@ -43,7 +43,7 @@ void LoginDialog::accept() // Setup the login task and start it m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); - m_loginTask = m_account->login(nullptr, ui->passTextBox->text()); + m_loginTask = m_account->login(ui->passTextBox->text()); connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 15c04061..f46aa3b9 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -37,7 +37,7 @@ int MSALoginDialog::exec() { // Setup the login task and start it m_account = MinecraftAccount::createBlankMSA(); - m_loginTask = m_account->loginMSA(nullptr); + m_loginTask = m_account->loginMSA(); connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index d3e2b9a4..76b6af49 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -25,8 +25,8 @@ #include "ui/dialogs/ProgressDialog.h" #include <Application.h> -#include "minecraft/auth/flows/AuthRequest.h" -#include "minecraft/auth/flows/Parsers.h" +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent) @@ -150,6 +150,9 @@ void ProfileSetupDialog::checkFinished( QByteArray data, QList<QNetworkReply::RawHeaderPair> headers ) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + if(error == QNetworkReply::NoError) { auto doc = QJsonDocument::fromJson(data); auto root = doc.object(); @@ -231,6 +234,9 @@ void ProfileSetupDialog::setupProfileFinished( QByteArray data, QList<QNetworkReply::RawHeaderPair> headers ) { + auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); + requestor->deleteLater(); + isWorking = false; if(error == QNetworkReply::NoError) { /* diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 4e6142fa..6a5a324f 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -20,16 +20,6 @@ void SkinUploadDialog::on_buttonBox_rejected() void SkinUploadDialog::on_buttonBox_accepted() { - AuthSessionPtr session = std::make_shared<AuthSession>(); - auto login = m_acct->refresh(session); - ProgressDialog prog(this); - if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) - { - //FIXME: recover with password prompt - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to login!"), QMessageBox::Warning)->exec(); - close(); - return; - } QString fileName; QString input = ui->skinPathTextBox->text(); QRegExp urlPrefixMatcher("^([a-z]+)://.+$"); @@ -91,11 +81,12 @@ void SkinUploadDialog::on_buttonBox_accepted() { model = SkinUpload::ALEX; } + ProgressDialog prog(this); SequentialTask skinUpload; - skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, session, FS::read(fileName), model))); + skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); auto selectedCape = ui->capeCombo->currentData().toString(); if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { - skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, session, selectedCape))); + skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape))); } if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 816dce47..d3eb2655 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -170,13 +170,7 @@ void AccountListPage::on_actionRefresh_triggered() { if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); - AuthSessionPtr session = std::make_shared<AuthSession>(); - auto task = account->refresh(session); - if (task) { - ProgressDialog progDialog(this); - progDialog.execWithTask(task.get()); - // TODO: respond to results of the task - } + m_accounts->requestRefresh(account->internalId()); } } @@ -244,15 +238,9 @@ void AccountListPage::on_actionDeleteSkin_triggered() return; QModelIndex selected = selection.first(); - AuthSessionPtr session = std::make_shared<AuthSession>(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); - auto login = account->refresh(session); ProgressDialog prog(this); - if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec(); - return; - } - auto deleteSkinTask = std::make_shared<SkinDelete>(this, session); + auto deleteSkinTask = std::make_shared<SkinDelete>(this, account->accessToken()); if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); return; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 715059ff..6e57909b 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -215,9 +215,6 @@ void VersionPage::updateVersionControls() bool supportsFabric = minecraftVersion >= Version("1.14"); ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); - bool supportsForge = minecraftVersion <= Version("1.16.5"); - ui->actionInstall_Forge->setEnabled(controlsEnabled && supportsForge); - bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index b5d8f22b..3a97d477 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "AtlFilterModel.h" #include <QDebug> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h index bd72ad91..5235ccdb 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <QtCore/QSortFilterProxyModel> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index e8c6deee..82e383ca 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "AtlListModel.h" #include <BuildConfig.h> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h index 79aa8180..2574c48d 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <QAbstractListModel> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 14bbd18b..ac3869dc 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "AtlOptionalModDialog.h" #include "ui_AtlOptionalModDialog.h" diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index a1df43f6..9832014c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 5f6a1396..af0cc8d6 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Philip T <me@phit.link> + * + * 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 "AtlPage.h" #include "ui_AtlPage.h" diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index b95b3d9e..5b3f2228 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -1,4 +1,5 @@ -/* Copyright 2013-2019 MultiMC Contributors +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp index 793b8769..67e2277c 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "FtbFilterModel.h" #include <QDebug> diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h index 2e712c7d..1be28e99 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <QtCore/QSortFilterProxyModel> diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index 59cd0b85..be9e1760 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "FtbListModel.h" #include "BuildConfig.h" diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.h b/launcher/ui/pages/modplatform/ftb/FtbListModel.h index e2b73c25..314cb789 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.h +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.h @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <QAbstractListModel> diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index a82de1d6..b6b5dcd4 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2021 Philip T <me@phit.link> + * + * 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 "FtbPage.h" #include "ui_FtbPage.h" diff --git a/launcher/ui/widgets/ErrorFrame.cpp b/launcher/ui/widgets/ErrorFrame.cpp new file mode 100644 index 00000000..b3e41036 --- /dev/null +++ b/launcher/ui/widgets/ErrorFrame.cpp @@ -0,0 +1,134 @@ +/* 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 <QMessageBox> +#include <QtGui> + +#include "ErrorFrame.h" +#include "ui_ErrorFrame.h" + +#include "ui/dialogs/CustomMessageBox.h" + +void ErrorFrame::clear() +{ + setTitle(QString()); + setDescription(QString()); +} + +ErrorFrame::ErrorFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::ErrorFrame) +{ + ui->setupUi(this); + ui->label_Description->setHidden(true); + ui->label_Title->setHidden(true); + updateHiddenState(); +} + +ErrorFrame::~ErrorFrame() +{ + delete ui; +} + +void ErrorFrame::updateHiddenState() +{ + if(ui->label_Description->isHidden() && ui->label_Title->isHidden()) + { + setHidden(true); + } + else + { + setHidden(false); + } +} + +void ErrorFrame::setTitle(QString text) +{ + if(text.isEmpty()) + { + ui->label_Title->setHidden(true); + } + else + { + ui->label_Title->setText(text); + ui->label_Title->setHidden(false); + } + updateHiddenState(); +} + +void ErrorFrame::setDescription(QString text) +{ + if(text.isEmpty()) + { + ui->label_Description->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->label_Description->setHidden(false); + updateHiddenState(); + } + ui->label_Description->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->label_Description->setOpenExternalLinks(false); + ui->label_Description->setTextFormat(Qt::TextFormat::RichText); + desc = text; + // This allows injecting HTML here. + labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>"); + QObject::connect(ui->label_Description, &QLabel::linkActivated, this, &ErrorFrame::ellipsisHandler); + } + else + { + ui->label_Description->setTextFormat(Qt::TextFormat::PlainText); + labeltext.append(finaltext); + } + ui->label_Description->setText(labeltext); +} + +void ErrorFrame::ellipsisHandler(const QString &link) +{ + if(!currentBox) + { + currentBox = CustomMessageBox::selectable(this, QString(), desc); + connect(currentBox, &QMessageBox::finished, this, &ErrorFrame::boxClosed); + currentBox->show(); + } + else + { + currentBox->setText(desc); + } +} + +void ErrorFrame::boxClosed(int result) +{ + currentBox = nullptr; +} diff --git a/launcher/ui/widgets/ErrorFrame.h b/launcher/ui/widgets/ErrorFrame.h new file mode 100644 index 00000000..d5069a14 --- /dev/null +++ b/launcher/ui/widgets/ErrorFrame.h @@ -0,0 +1,49 @@ +/* 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 <QFrame> + +namespace Ui +{ +class ErrorFrame; +} + +class ErrorFrame : public QFrame +{ + Q_OBJECT + +public: + explicit ErrorFrame(QWidget *parent = 0); + ~ErrorFrame(); + + void setTitle(QString text); + void setDescription(QString text); + + void clear(); + +public slots: + void ellipsisHandler(const QString& link ); + void boxClosed(int result); + +private: + void updateHiddenState(); + +private: + Ui::ErrorFrame *ui; + QString desc; + class QMessageBox * currentBox = nullptr; +}; diff --git a/launcher/ui/widgets/ErrorFrame.ui b/launcher/ui/widgets/ErrorFrame.ui new file mode 100644 index 00000000..0bb56743 --- /dev/null +++ b/launcher/ui/widgets/ErrorFrame.ui @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ErrorFrame</class> + <widget class="QFrame" name="ErrorFrame"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>527</width> + <height>113</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>120</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_Title"> + <property name="text"> + <string notr="true"/> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_Description"> + <property name="toolTip"> + <string notr="true"/> + </property> + <property name="text"> + <string notr="true"/> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index dba5a1ae..73b4b5c6 100644 --- a/libraries/javacheck/CMakeLists.txt +++ b/libraries/javacheck/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.1) project(launcher Java) -find_package(Java 1.6 REQUIRED COMPONENTS Development) +find_package(Java 7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT JavaCheck) -set(CMAKE_JAVA_COMPILE_FLAGS -target 1.6 -source 1.6 -Xlint:deprecation -Xlint:unchecked) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) set(SRC JavaCheck.java diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index a64c601d..4db80de2 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.1) project(launcher Java) -find_package(Java 1.6 REQUIRED COMPONENTS Development) +find_package(Java 7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT org.multimc.EntryPoint) -set(CMAKE_JAVA_COMPILE_FLAGS -target 1.6 -source 1.6 -Xlint:deprecation -Xlint:unchecked) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) set(SRC org/multimc/EntryPoint.java |