diff options
160 files changed, 3042 insertions, 1694 deletions
@@ -21,6 +21,10 @@ Debug build /build-* +# Install dirs +install +/install-* + # Ctags File tags @@ -15,8 +15,14 @@ MultiMC is a portable application and is not supposed to be installed into any s That would be anything outside your home folder. Before running `make install`, make sure you set the install path to something you have write access to. Never build this under an administrator/root level account. Don't use `sudo`. It won't work and it's not supposed to work. -Also note that this guide is for development purposes only. No support is given for building your own fork or special build for any reason whatsoever. +Also note that this guide is for development purposes only. +**No support is given for building your own fork or special build for any reason whatsoever**. +# Branding, identifying marks and API keys + +The logo and related assets are All Rights Reserved and may only be used in official builds of MultiMC hosted on multimc.org, and as such, are not, and will not be included in this repository. The source is only provided for the purpose of collaboration. + +API keys are necessary for Microsoft account functionality. More info in [(Not) Secrets](https://github.com/MultiMC/Launcher/tree/develop/notsecrets) # Getting the source @@ -102,7 +108,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`. @@ -176,11 +182,19 @@ zlib1.dll # macOS ### Install prerequisites: -- Install XCode and set it up to the point where you can build things from a terminal +- Install XCode Command Line tools - Install the official build of CMake (https://cmake.org/download/) - Install JDK 8 (https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html) - Get Qt 5.6 and install it (https://download.qt.io/new_archive/qt/5.6/5.6.3/) +### XCode Command Line tools + +If you don't have XCode CommandLine tools installed, you can install them by using this command in the Terminal App + +```bash +xcode-select --install +``` + ### Build Pick an installation path - this is where the final `.app` will be constructed when you run `make install`. Supply it as the `CMAKE_INSTALL_PREFIX` argument during CMake configuration. diff --git a/CMakeLists.txt b/CMakeLists.txt index e45dbf7c..d1c6ac3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,9 @@ if(UNIX AND APPLE) endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") +# Fix build with Qt 5.13 +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") + ##################################### Set Application options ##################################### ######## Set URLs ######## @@ -55,7 +58,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.") @@ -2,8 +2,8 @@ <img src="https://avatars2.githubusercontent.com/u/5411890" alt="MultiMC logo"/> </p> -MultiMC 5 -========= +MultiMC +======= MultiMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity. @@ -15,7 +15,7 @@ While blindly submitting PRs is definitely possible, they're not necessarily goi We aren't looking for flashy features, but expanding upon the existing feature set without distruption or endangering future viability of the project is OK. ### Building -If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build instructions. +If you want to build the launcher yourself, check [BUILD.md](BUILD.md) for build instructions. ### Code formatting Just follow the existing formatting. @@ -27,54 +27,27 @@ In general, in order of importance: * Indent with 4 space unless it's in a submodule. * Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both. - ## Translations 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. +## License +Copyright © 2013-2022 MultiMC Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this program except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). -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). +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. -Apache covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork *without* implying that you have our blessing. +## Forking/Redistributing/Custom builds policy +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. +The license gives you access to the source MultiMC is build from, but: +- Not the name, logo and other branding. +- Not the API tokens required to talk to services the launcher depends on. -## License -Copyright © 2013-2021 MultiMC Contributors +Because of the nature of the agreements required to interact with the Microsoft identity platform, it's impossible for us to continue allowing everyone to build the code as 'MultiMC'. The source code has been debranded and now builds as `DevLauncher` by default. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this program except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). +You must provide your own branding if you want to distribute your own builds. -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. +You will also have to register your own app on Azure to be able to handle Microsoft account logins. -## Build status -### Linux (Intel32) -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_Linux32_Build&guest=1"> -Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_Linux32_Build)/statusIcon"/> -</a> -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_Linux32_Deploy&guest=1"> -Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_Linux32_Deploy)/statusIcon"/> -</a> - -### Linux (AMD64) -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_Linux64_Build&guest=1"> -Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_Linux64_Build)/statusIcon"/> -</a> -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_Linux64_Deploy&guest=1"> -Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_Linux64_Deploy)/statusIcon"/> -</a> - -### macOS (AMD64) -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_MacOS_Build&guest=1"> -Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_MacOS_Build)/statusIcon"/> -</a> -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_MacOS_Deploy&guest=1"> -Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_MacOS_Deploy)/statusIcon"/> -</a> - -### Windows (Intel32) -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_Windows_Build&guest=1"> -Build: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_Windows_Build)/statusIcon"/> -</a> -<a href="https://teamcity.multimc.org/viewType.html?buildTypeId=Launcher_Launcher_Windows_Deploy&guest=1"> -Deploy: <img src="https://teamcity.multimc.org/app/rest/builds/buildType:(id:Launcher_Launcher_Windows_Deploy)/statusIcon"/> -</a> +If you decide to fork the project, a mention of its origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork *without* implying that you have our blessing. 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..958c5e3d 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; } @@ -512,7 +521,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Set up paths { // Root path is used for updates. -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QDir foo(FS::PathCombine(binPath, "..")); m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) @@ -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/DesktopServices.cpp b/launcher/DesktopServices.cpp index 5368ddc8..dcc1b0ce 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -7,7 +7,7 @@ /** * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. */ -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #include <unistd.h> #include <errno.h> @@ -83,7 +83,7 @@ bool openDirectory(const QString &path, bool ensureExists) { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) return IndirectOpen(f); #else return f(); @@ -97,7 +97,7 @@ bool openFile(const QString &path) { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }; -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) return IndirectOpen(f); #else return f(); @@ -107,7 +107,7 @@ bool openFile(const QString &path) bool openFile(const QString &application, const QString &path, const QString &workingDirectory, qint64 *pid) { qDebug() << "Opening file" << path << "using" << application; -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave return IndirectOpen([&]() { @@ -121,7 +121,7 @@ bool openFile(const QString &application, const QString &path, const QString &wo bool run(const QString &application, const QStringList &args, const QString &workingDirectory, qint64 *pid) { qDebug() << "Running" << application << "with args" << args.join(' '); -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave return IndirectOpen([&]() { @@ -139,7 +139,7 @@ bool openUrl(const QUrl &url) { return QDesktopServices::openUrl(url); }; -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) return IndirectOpen(f); #else return f(); diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 13f05b86..6de20de6 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -403,7 +403,7 @@ QString getDesktopDir() bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) { -#if defined Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) location = PathCombine(location, name + ".desktop"); QFile f(location); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index d0a63fe3..8cd68d7b 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -55,14 +55,14 @@ void InstanceImportTask::executeTask() const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); - m_filesNetJob.reset(new NetJob(tr("Modpack download"))); + m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed); - m_filesNetJob->start(APPLICATION->network()); + m_filesNetJob->start(); } } @@ -337,7 +337,7 @@ void InstanceImportTask::processFlame() connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]() { auto results = m_modIdResolver->getResults(); - m_filesNetJob.reset(new NetJob(tr("Mod download"))); + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); for(auto result: results.files) { QString filename = result.fileName; @@ -391,7 +391,7 @@ void InstanceImportTask::processFlame() setProgress(current, total); }); setStatus(tr("Downloading mods...")); - m_filesNetJob->start(APPLICATION->network()); + m_filesNetJob->start(); } ); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 8bd5732f..7750be1a 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -16,6 +16,7 @@ #include <QHostInfo> #include <QList> #include <QHostAddress> +#include <QPushButton> #include "BuildConfig.h" #include "JavaCommon.h" @@ -35,6 +36,8 @@ void LaunchController::executeTask() return; } + JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); + login(); } @@ -90,8 +93,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 +114,115 @@ 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; - } - passDialog.setUsername(username); - if (passDialog.exec() == QDialog::Accepted) - { - password = passDialog.password(); - } - else - { - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); + if (name.length()) + { + usedname = name; + } + m_session->MakeOffline(usedname); + // offline flavored game from here :3 } - break; - } - 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); + if(m_accountToUse->ownsMinecraft()) { + if(!m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() == QDialog::Accepted) + { + tryagain = true; + continue; + } + else + { + emitFailed(tr("Received undetermined session status during login.")); + return; + } + } + // we own Minecraft, there is a profile, it's all ready to go! + launchInstance(); return; } - // Now handle setting up a profile name here... - ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); - if (dialog.exec() == QDialog::Accepted) - { - tryagain = true; - continue; + else { + // play demo ? + QMessageBox box(m_parentWidget); + box.setWindowTitle(tr("Play demo?")); + box.setText(tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play the demo?")); + box.setIcon(QMessageBox::Warning); + auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + box.setDefaultButton(cancelButton); + + box.exec(); + if(box.clickedButton() == demoButton) { + // play demo here + m_session->MakeDemo(); + launchInstance(); + } + else { + emitFailed(tr("Launch cancelled - account does not own Minecraft.")); + } } - else + return; + } + 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 +231,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 +299,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/UpdateController.cpp b/launcher/UpdateController.cpp index 136e22fd..f9b7d349 100644 --- a/launcher/UpdateController.cpp +++ b/launcher/UpdateController.cpp @@ -93,7 +93,7 @@ void UpdateController::installUpdates() qDebug() << "Installing updates."; #ifdef Q_OS_WIN QString finishCmd = QApplication::applicationFilePath(); -#elif defined Q_OS_LINUX +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QString finishCmd = FS::PathCombine(m_root, BuildConfig.LAUNCHER_NAME); #elif defined Q_OS_MAC QString finishCmd = QApplication::applicationFilePath(); diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 6392a50f..b9090e29 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -78,7 +78,7 @@ void Version::parse() // FIXME: this is bad. versions can contain a lot more separators... QStringList parts = m_string.split('.'); - for (const auto &part : parts) + for (const auto& part : parts) { m_sections.append(Section(part)); } diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 80c599cc..35ddc35c 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -103,11 +103,15 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) for(QString line : lines) { line = line.trimmed(); + // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux + if (line.contains("/bedrock/strata")) { + continue; + } auto parts = line.split('=', QString::SkipEmptyParts); if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { - success = false; + continue; } else { diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 6b58db37..fd7e43e9 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -31,7 +31,7 @@ JavaUtils::JavaUtils() { } -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH) { QDir mmcBin(QCoreApplication::applicationDirPath()); @@ -83,7 +83,7 @@ QProcessEnvironment CleanEnviroment() qDebug() << "Env: ignoring" << key << value; continue; } -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) // Do not pass LD_* variables to java. They were intended for MultiMC if(key.startsWith("LD_")) { @@ -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/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index a9d62fcd..84155922 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -117,7 +117,7 @@ void Meta::BaseEntity::load(Net::Mode loadType) { return; } - m_updateTask = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename())); + m_updateTask = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()); auto url = this->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); entry->setStale(true); @@ -140,7 +140,7 @@ void Meta::BaseEntity::load(Net::Mode loadType) m_updateStatus = UpdateStatus::Failed; m_updateTask.reset(); }); - m_updateTask->start(APPLICATION->network()); + m_updateTask->start(); } bool Meta::BaseEntity::isLoaded() const diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 1c65a212..7290aeb4 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -29,6 +29,8 @@ #include "net/ChecksumValidator.h" #include "BuildConfig.h" +#include "Application.h" + namespace { QSet<QString> collectPathsFromDir(QString dirPath) { @@ -318,7 +320,7 @@ QString AssetObject::getRelPath() NetJob::Ptr AssetsIndex::getDownloadJob() { - auto job = new NetJob(QObject::tr("Assets for %1").arg(id)); + auto job = new NetJob(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); for (auto &object : objects.values()) { auto dl = object.getDownloadAction(); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 4c16e572..0b3c049b 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"); } @@ -431,8 +431,7 @@ QStringList MinecraftInstance::processMinecraftArgs( QMap<QString, QString> token_mapping; // yggdrasil! - if(session) - { + if(session) { // token_mapping["auth_username"] = session->username; token_mapping["auth_session"] = session->session; token_mapping["auth_access_token"] = session->access_token; @@ -440,6 +439,9 @@ QStringList MinecraftInstance::processMinecraftArgs( token_mapping["auth_uuid"] = session->uuid; token_mapping["user_properties"] = session->serializeUserProperties(); token_mapping["user_type"] = session->user_type; + if(session->demo) { + args_pattern += " --demo"; + } } // blatant self-promotion. @@ -872,7 +874,9 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt // if we aren't in offline mode,. if(session->status != AuthSession::PlayableOffline) { - process->appendStep(new ClaimAccount(pptr, session)); + if(!session->demo) { + process->appendStep(new ClaimAccount(pptr, session)); + } process->appendStep(new Update(pptr, Net::Mode::Online)); } else @@ -961,7 +965,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/OpSys.cpp b/launcher/minecraft/OpSys.cpp index f6a4ed1c..093ec419 100644 --- a/launcher/minecraft/OpSys.cpp +++ b/launcher/minecraft/OpSys.cpp @@ -17,6 +17,8 @@ OpSys OpSys_fromString(QString name) { + if (name == "freebsd") + return Os_FreeBSD; if (name == "linux") return Os_Linux; if (name == "windows") @@ -30,6 +32,8 @@ QString OpSys_toString(OpSys name) { switch (name) { + case Os_FreeBSD: + return "freebsd"; case Os_Linux: return "linux"; case Os_OSX: diff --git a/launcher/minecraft/OpSys.h b/launcher/minecraft/OpSys.h index 63c750b1..0936f817 100644 --- a/launcher/minecraft/OpSys.h +++ b/launcher/minecraft/OpSys.h @@ -18,6 +18,7 @@ enum OpSys { Os_Windows, + Os_FreeBSD, Os_Linux, Os_OSX, Os_Other @@ -27,11 +28,11 @@ OpSys OpSys_fromString(QString); QString OpSys_toString(OpSys); #ifdef Q_OS_WIN32 -#define currentSystem Os_Windows + #define currentSystem Os_Windows +#elif defined Q_OS_MAC + #define currentSystem Os_OSX +#elif defined Q_OS_FREEBSD + #define currentSystem Os_FreeBSD #else -#ifdef Q_OS_MAC -#define currentSystem Os_OSX -#else -#define currentSystem Os_Linux + #define currentSystem Os_Linux #endif -#endif
\ No newline at end of file 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..feface80 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_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 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/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index d44f9098..6bea74a3 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -30,3 +30,8 @@ bool AuthSession::MakeOffline(QString offline_playername) status = PlayableOffline; return true; } + +void AuthSession::MakeDemo() { + player_name = "Player"; + demo = true; +} diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 55fbdf39..a75df506 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -11,6 +11,7 @@ class QNetworkAccessManager; struct AuthSession { bool MakeOffline(QString offline_playername); + void MakeDemo(); QString serializeUserProperties(); @@ -43,6 +44,9 @@ struct AuthSession bool auth_server_online = false; // Did the user request online mode? bool wants_online = true; + + //Is this a demo session? + bool demo = false; }; typedef std::shared_ptr<AuthSession> AuthSessionPtr; 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..2dd36562 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; @@ -94,7 +94,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char return false; } if(!getString(obj.value("Token"), output.token)) { - qWarning() << "User Token is not a timestamp"; + qWarning() << "User Token is not a string"; return false; } auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); @@ -226,6 +226,8 @@ bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) } auto obj = doc.object(); + output.canPlayMinecraft = false; + output.ownsMinecraft = false; auto itemsArray = obj.value("items").toArray(); for(auto item: itemsArray) { 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..add91659 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -0,0 +1,91 @@ +#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) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + 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/launch/ClaimAccount.cpp b/launcher/minecraft/launch/ClaimAccount.cpp index bb4f6806..1cd7c0da 100644 --- a/launcher/minecraft/launch/ClaimAccount.cpp +++ b/launcher/minecraft/launch/ClaimAccount.cpp @@ -6,7 +6,7 @@ ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session): LaunchStep(parent) { - if(session->status == AuthSession::Status::PlayableOnline) + if(session->status == AuthSession::Status::PlayableOnline && !session->demo) { auto accounts = APPLICATION->accounts(); m_account = accounts->getAccountByProfileName(session->player_name); diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index d57499aa..8cd439b1 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -23,6 +23,13 @@ #include "FileSystem.h" #include <QDir> +#ifdef major + #undef major +#endif +#ifdef minor + #undef minor +#endif + static QString replaceSuffix (QString target, const QString &suffix, const QString &replacement) { if (!target.endsWith(suffix)) diff --git a/launcher/minecraft/launch/PrintInstanceInfo.cpp b/launcher/minecraft/launch/PrintInstanceInfo.cpp index 3da17902..e8fbcb9b 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.cpp +++ b/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -19,8 +19,9 @@ #include "PrintInstanceInfo.h" #include <launch/LaunchTask.h> -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) namespace { +#if defined(Q_OS_LINUX) void probeProcCpuinfo(QStringList &log) { std::ifstream cpuin("/proc/cpuinfo"); @@ -66,7 +67,43 @@ void runLspci(QStringList &log) } pclose(lspci); } +#elif defined(Q_OS_FREEBSD) +void runSysctlHwModel(QStringList &log) +{ + char buff[512]; + FILE *hwmodel = popen("sysctl hw.model", "r"); + while (fgets(buff, 512, hwmodel) != NULL) + { + log << QString::fromUtf8(buff); + break; + } + pclose(hwmodel); +} +void runPciconf(QStringList &log) +{ + char buff[512]; + std::string strcard; + FILE *pciconf = popen("pciconf -lv -a vgapci0", "r"); + while (fgets(buff, 512, pciconf) != NULL) + { + if (strncmp(buff, " vendor", 10) == 0) + { + std::string str(buff); + strcard.append(str.substr(str.find_first_of("'") + 1, str.find_last_not_of("'") - (str.find_first_of("'") + 2))); + strcard.append(" "); + } + else if (strncmp(buff, " device", 10) == 0) + { + std::string str2(buff); + strcard.append(str2.substr(str2.find_first_of("'") + 1, str2.find_last_not_of("'") - (str2.find_first_of("'") + 2))); + } + log << QString::fromStdString(strcard); + break; + } + pclose(pciconf); +} +#endif void runGlxinfo(QStringList & log) { // FIXME: fixed size buffers... @@ -94,10 +131,14 @@ void PrintInstanceInfo::executeTask() auto instance = m_parent->instance(); QStringList log; -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) ::probeProcCpuinfo(log); ::runLspci(log); ::runGlxinfo(log); +#elif defined(Q_OS_FREEBSD) + ::runSysctlHwModel(log); + ::runPciconf(log); + ::runGlxinfo(log); #endif logLines(log, MessageLevel::Launcher); diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index d9f7ecdc..15acf678 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -5,6 +5,13 @@ #include <minecraft/PackProfile.h> #include <minecraft/VersionFilterData.h> +#ifdef major + #undef major +#endif +#ifdef minor + #undef minor +#endif + void VerifyJavaInstall::executeTask() { auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance()); 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/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index 096e1719..c4bddb08 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -24,7 +24,10 @@ void AssetUpdateTask::executeTask() auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; QString localPath = assets->id + ".json"; - auto job = new NetJob(tr("Asset index for %1").arg(m_inst->name())); + auto job = new NetJob( + tr("Asset index for %1").arg(m_inst->name()), + APPLICATION->network() + ); auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", localPath); @@ -43,7 +46,7 @@ void AssetUpdateTask::executeTask() connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); qDebug() << m_inst->name() << ": Starting asset index download"; - downloadJob->start(APPLICATION->network()); + downloadJob->start(); } bool AssetUpdateTask::canAbort() const @@ -78,7 +81,7 @@ void AssetUpdateTask::assetIndexFinished() connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); - downloadJob->start(APPLICATION->network()); + downloadJob->start(); return; } emitSucceeded(); diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp index a5c6b1e3..58141991 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -61,7 +61,7 @@ void FMLLibrariesTask::executeTask() // download missing libs to our place setStatus(tr("Downloading FML libraries...")); - auto dljob = new NetJob("FML libraries"); + auto dljob = new NetJob("FML libraries", APPLICATION->network()); auto metacache = APPLICATION->metacache(); for (auto &lib : fmlLibsToProcess) { @@ -74,7 +74,7 @@ void FMLLibrariesTask::executeTask() connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress); downloadJob.reset(dljob); - downloadJob->start(APPLICATION->network()); + downloadJob->start(); } bool FMLLibrariesTask::canAbort() const diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 065b4e06..667dd5d9 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -20,7 +20,7 @@ void LibrariesTask::executeTask() auto components = inst->getPackProfile(); auto profile = components->getProfile(); - auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); + auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()); downloadJob.reset(job); auto metacache = APPLICATION->metacache(); @@ -65,7 +65,7 @@ void LibrariesTask::executeTask() connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); - downloadJob->start(APPLICATION->network()); + downloadJob->start(); } bool LibrariesTask::canAbort() const 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..e5db512e 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> @@ -41,12 +58,12 @@ bool PackInstallTask::abort() void PackInstallTask::executeTask() { qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); - auto *netJob = new NetJob("ATLauncher::VersionFetch"); + auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network()); auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") .arg(m_pack).arg(m_version_name); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); @@ -407,7 +424,7 @@ void PackInstallTask::installConfigs() { qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); setStatus(tr("Downloading configs...")); - jobPtr.reset(new NetJob(tr("Config download"))); + jobPtr = new NetJob(tr("Config download"), APPLICATION->network()); auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") @@ -441,7 +458,7 @@ void PackInstallTask::installConfigs() setProgress(current, total); }); - jobPtr->start(APPLICATION->network()); + jobPtr->start(); } void PackInstallTask::extractConfigs() @@ -491,7 +508,7 @@ void PackInstallTask::downloadMods() setStatus(tr("Downloading mods...")); jarmods.clear(); - jobPtr.reset(new NetJob(tr("Mod download"))); + jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); for(const auto& mod : m_version.mods) { // skip non-client mods if(!mod.client) continue; @@ -596,7 +613,7 @@ void PackInstallTask::downloadMods() setProgress(current, total); }); - jobPtr->start(APPLICATION->network()); + jobPtr->start(); } void PackInstallTask::onModsDownloaded() { 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/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 06f0cf2b..3889a935 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -14,7 +14,7 @@ void Flame::FileResolvingTask::executeTask() { setStatus(tr("Resolving mod IDs...")); setProgress(0, m_toProcess.files.size()); - m_dljob = new NetJob("Mod id resolver"); + m_dljob = new NetJob("Mod id resolver", m_network); results.resize(m_toProcess.files.size()); int index = 0; for(auto & file: m_toProcess.files) @@ -27,7 +27,7 @@ void Flame::FileResolvingTask::executeTask() index ++; } connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished); - m_dljob->start(m_network); + m_dljob->start(); } void Flame::FileResolvingTask::netJobFinished() diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index ecf36188..961fe868 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -12,7 +12,7 @@ void PackFetchTask::fetch() publicPacks.clear(); thirdPartyPacks.clear(); - jobPtr = new NetJob("LegacyFTB::ModpackFetch"); + jobPtr = new NetJob("LegacyFTB::ModpackFetch", m_network); QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); @@ -25,7 +25,7 @@ void PackFetchTask::fetch() QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); - jobPtr->start(m_network); + jobPtr->start(); } void PackFetchTask::fetchPrivate(const QStringList & toFetch) @@ -35,7 +35,7 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch) for (auto &packCode: toFetch) { QByteArray *data = new QByteArray(); - NetJob *job = new NetJob("Fetching private pack"); + NetJob *job = new NetJob("Fetching private pack", m_network); job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data)); QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] @@ -63,7 +63,7 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch) delete data; }); - job->start(m_network); + job->start(); } } diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 64aecb39..1d300192 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -33,7 +33,7 @@ void PackInstallTask::downloadPack() auto packoffset = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", packoffset); - netJobContainer = new NetJob("Download FTB Pack"); + netJobContainer = new NetJob("Download FTB Pack", m_network); entry->setStale(true); QString url; @@ -51,7 +51,7 @@ void PackInstallTask::downloadPack() connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); - netJobContainer->start(m_network); + netJobContainer->start(); progress(1, 4); } diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 563b5cfa..03570226 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" @@ -46,12 +63,11 @@ void PackInstallTask::executeTask() return; } - auto *netJob = new NetJob("ModpacksCH::VersionFetch"); - auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2") - .arg(m_pack.id).arg(version.id); + auto *netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network()); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); @@ -96,7 +112,7 @@ void PackInstallTask::downloadPack() { setStatus(tr("Downloading mods...")); - jobPtr = new NetJob(tr("Mod download")); + jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); for(auto file : m_version.files) { if(file.serverOnly) continue; @@ -142,7 +158,7 @@ void PackInstallTask::downloadPack() setProgress(current, total); }); - jobPtr->start(APPLICATION->network()); + jobPtr->start(); } void PackInstallTask::install() 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/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index 0ab9f3c0..9093b245 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -44,14 +44,14 @@ void Technic::SingleZipPackInstallTask::executeTask() const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); - m_filesNetJob.reset(new NetJob(tr("Modpack download"))); + m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); - m_filesNetJob->start(APPLICATION->network()); + m_filesNetJob->start(); } void Technic::SingleZipPackInstallTask::downloadSucceeded() diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 2492ee81..b5c91582 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -42,12 +42,12 @@ bool Technic::SolderPackInstallTask::abort() { void Technic::SolderPackInstallTask::executeTask() { setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); - m_filesNetJob.reset(new NetJob(tr("Finding recommended version"))); + m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network); m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); - m_filesNetJob->start(m_network); + m_filesNetJob->start(); } void Technic::SolderPackInstallTask::versionSucceeded() @@ -67,12 +67,12 @@ void Technic::SolderPackInstallTask::versionSucceeded() } setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString())); - m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"))); + m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); - m_filesNetJob->start(m_network); + m_filesNetJob->start(); } void Technic::SolderPackInstallTask::fileListSucceeded() @@ -99,7 +99,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() m_filesNetJob.reset(); return; } - m_filesNetJob.reset(new NetJob(tr("Downloading modpack"))); + m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); int i = 0; for (auto &modUrl: modUrls) { @@ -113,7 +113,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); - m_filesNetJob->start(m_network); + m_filesNetJob->start(); } void Technic::SolderPackInstallTask::downloadSucceeded() diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index 45b9bc0f..fdea710f 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -29,7 +29,7 @@ class NetJob : public Task public: using Ptr = shared_qobject_ptr<NetJob>; - explicit NetJob(QString job_name) : Task() + explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : Task(), m_network(network) { setObjectName(job_name); } @@ -65,19 +65,6 @@ private slots: public slots: virtual void executeTask() override; virtual bool abort() override; - virtual void start(shared_qobject_ptr<QNetworkAccessManager> network) { - m_network = network; - start(); - } - -protected slots: - void start() override { - if(!m_network) { - throw "Missing network while trying to start " + objectName(); - return; - } - Task::start(); - } private slots: void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 77d428a5..4f4359b8 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -37,12 +37,12 @@ void NewsChecker::reloadNews() qDebug() << "Reloading news."; - NetJob* job = new NetJob("News RSS Feed"); + NetJob* job = new NetJob("News RSS Feed", m_network); job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData)); QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); - job->start(m_network); + job->start(); } void NewsChecker::rssDownloadFinished() diff --git a/launcher/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp index 00c918f8..c08bcdcb 100644 --- a/launcher/notifications/NotificationChecker.cpp +++ b/launcher/notifications/NotificationChecker.cpp @@ -52,12 +52,12 @@ void NotificationChecker::checkForNotifications() { return; } - m_checkJob.reset(new NetJob("Checking for notifications")); + m_checkJob = new NetJob("Checking for notifications", APPLICATION->network()); auto entry = APPLICATION->metacache()->resolveEntry("root", "notifications.json"); entry->setStale(true); m_checkJob->addNetAction(m_download = Net::Download::makeCached(m_notificationsUrl, entry)); connect(m_download.get(), &Net::Download::succeeded, this, &NotificationChecker::downloadSucceeded); - m_checkJob->start(APPLICATION->network()); + m_checkJob->start(); } void NotificationChecker::downloadSucceeded(int) diff --git a/launcher/package/rpm/MultiMC5.spec b/launcher/package/rpm/MultiMC5.spec index 20839f11..4b7e5002 100644 --- a/launcher/package/rpm/MultiMC5.spec +++ b/launcher/package/rpm/MultiMC5.spec @@ -1,14 +1,20 @@ Name: MultiMC5 Version: 1.4 -Release: 3%{?dist} +Release: 4%{?dist} Summary: A local install wrapper for MultiMC License: ASL 2.0 URL: https://multimc.org -BuildArch: x86_64 +ExclusiveArch: %{ix86} x86_64 + +BuildRequires: desktop-file-utils +BuildRequires: libappstream-glib +Requires: zenity %{?suse_version:lib}qt5-qtbase wget xrandr +Provides: multimc = %{version} +Provides: MultiMC = %{version} +Provides: multimc5 = %{version} + -Requires: zenity qt5-qtbase wget xrandr -Provides: multimc MultiMC multimc5 %description A local install wrapper for MultiMC @@ -23,22 +29,29 @@ mkdir -p %{buildroot}/opt/multimc install -m 0644 ../ubuntu/multimc/opt/multimc/icon.svg %{buildroot}/opt/multimc/icon.svg install -m 0755 ../ubuntu/multimc/opt/multimc/run.sh %{buildroot}/opt/multimc/run.sh mkdir -p %{buildroot}/%{_datadir}/applications -install -m 0644 ../ubuntu/multimc/usr/share/applications/multimc.desktop %{buildroot}/%{_datadir}/applications/multimc.desktop +desktop-file-install --dir=%{buildroot}%{_datadir}/applications ../ubuntu/multimc/usr/share/applications/multimc.desktop + mkdir -p %{buildroot}/%{_datadir}/metainfo -install -m 0644 ../ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml %{buildroot}/%{_datadir}/metainfo/multimc.metainfo.xml +install -m 0644 ../ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml %{buildroot}/%{_metainfodir}/multimc.metainfo.xml mkdir -p %{buildroot}/%{_mandir}/man1 install -m 0644 ../ubuntu/multimc/usr/share/man/man1/multimc.1 %{buildroot}/%{_mandir}/man1/multimc.1 +%check +appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/multimc.metainfo.xml + %files %dir /opt/multimc /opt/multimc/icon.svg /opt/multimc/run.sh %{_datadir}/applications/multimc.desktop -%{_datadir}/metainfo/multimc.metainfo.xml -%dir /usr/share/man/man1 -%{_mandir}/man1/multimc.1.gz +%{_metainfodir}/multimc.metainfo.xml +%dir %{_mandir}/man1 +%{_mandir}/man1/multimc.1* %changelog +* Fri Jan 28 2022 Jan Drögehoff <sentrycraft123@gmail.com> - 1.4-4 +- Update spec to support OpenSuse and conform to Fedora guidelines + * Sun Oct 03 2021 imperatorstorm <30777770+ImperatorStorm@users.noreply.github.com> - added manpage 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/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp index 880327c7..21e1a3b0 100644 --- a/launcher/tools/MCEditTool.cpp +++ b/launcher/tools/MCEditTool.cpp @@ -52,7 +52,7 @@ QString MCEditTool::getProgramPath() #else const QString mceditPath = path(); QDir mceditDir(mceditPath); -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) if (mceditDir.exists("mcedit.sh")) { return mceditDir.absoluteFilePath("mcedit.sh"); diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index ce53ac32..2e744007 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: { @@ -559,14 +573,14 @@ void TranslationsModel::downloadIndex() return; } qDebug() << "Downloading Translations Index..."; - d->m_index_job.reset(new NetJob("Translations Index")); + d->m_index_job = new NetJob("Translations Index", APPLICATION->network()); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); d->m_index_task = Net::Download::makeCached(QUrl("https://files.multimc.org/translations/index_v2.json"), entry); d->m_index_job->addNetAction(d->m_index_task); connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); - d->m_index_job->start(APPLICATION->network()); + d->m_index_job->start(); } void TranslationsModel::updateLanguage(QString key) @@ -611,13 +625,13 @@ void TranslationsModel::downloadTranslation(QString key) dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); dl->m_total_progress = lang->file_size; - d->m_dl_job.reset(new NetJob("Translation for " + key)); + d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network()); d->m_dl_job->addNetAction(dl); connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); - d->m_dl_job->start(APPLICATION->network()); + d->m_dl_job->start(); } void TranslationsModel::downloadNext() 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/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 501156d0..9795c38b 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -133,10 +133,10 @@ AboutDialog::~AboutDialog() void AboutDialog::loadPatronList() { - netJob = new NetJob("Patreon Patron List"); + netJob = new NetJob("Patreon Patron List", APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray(QUrl("https://files.multimc.org/patrons.txt"), &dataSink)); connect(netJob.get(), &NetJob::succeeded, this, &AboutDialog::patronListLoaded); - netJob->start(APPLICATION->network()); + netJob->start(); } void AboutDialog::patronListLoaded() 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/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp index 4a6a1fdd..c0f6074c 100644 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ b/launcher/ui/dialogs/UpdateDialog.cpp @@ -34,7 +34,7 @@ UpdateDialog::~UpdateDialog() void UpdateDialog::loadChangelog() { auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - dljob.reset(new NetJob("Changelog")); + dljob = new NetJob("Changelog", APPLICATION->network()); QString url; if(channel == "stable") { @@ -49,7 +49,7 @@ void UpdateDialog::loadChangelog() dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded); connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed); - dljob->start(APPLICATION->network()); + dljob->start(); } QString reprocessMarkdown(QByteArray markdown) diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 1f044866..25aec1ab 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -835,15 +835,6 @@ QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorActio if(group_index < 0) return current; - auto real_group = m_groups[group_index]; - int beginning_row = 0; - for(auto group: m_groups) - { - if(group == real_group) - break; - beginning_row += group->numRows(); - } - QPair<int, int> pos = cat->positionOf(current); int column = pos.first; int row = pos.second; 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/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index ccde78e7..4011d88c 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -315,7 +315,7 @@ void ScreenshotsPage::on_actionUpload_triggered() return; QList<ScreenShot::Ptr> uploaded; - auto job = NetJob::Ptr(new NetJob("Screenshot Upload")); + auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); if(selection.size() < 2) { auto item = selection.at(0); @@ -325,6 +325,7 @@ void ScreenshotsPage::on_actionUpload_triggered() m_uploadActive = true; ProgressDialog dialog(this); + if(dialog.execWithTask(job.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), @@ -356,7 +357,7 @@ void ScreenshotsPage::on_actionUpload_triggered() job->addNetAction(ImgurUpload::make(screenshot)); } SequentialTask task; - auto albumTask = NetJob::Ptr(new NetJob("Imgur Album Creation")); + auto albumTask = NetJob::Ptr(new NetJob("Imgur Album Creation", APPLICATION->network())); auto imgurAlbum = ImgurAlbumCreation::make(uploaded); albumTask->addNetAction(imgurAlbum); task.addTask(job); @@ -365,8 +366,12 @@ void ScreenshotsPage::on_actionUpload_triggered() ProgressDialog prog(this); if (prog.execWithTask(&task) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), - tr("Unknown error"), QMessageBox::Warning)->exec(); + CustomMessageBox::selectable( + this, + tr("Failed to upload screenshots!"), + tr("Unknown error"), + QMessageBox::Warning + )->exec(); } else { 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..ef9a9268 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> @@ -70,11 +86,11 @@ void ListModel::request() modpacks.clear(); endResetModel(); - auto *netJob = new NetJob("Atl::Request"); + auto *netJob = new NetJob("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); @@ -167,7 +183,7 @@ void ListModel::requestLogo(QString file, QString url) } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); - NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file)); + NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -185,7 +201,7 @@ void ListModel::requestLogo(QString file, QString url) emit logoFailed(file); }); - job->start(APPLICATION->network()); + job->start(); m_loadingLogos.append(file); } 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/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index a05ab641..891676cf 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -100,7 +100,7 @@ void ListModel::requestLogo(QString logo, QString url) } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo)); + NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -118,7 +118,7 @@ void ListModel::requestLogo(QString logo, QString url) emit logoFailed(logo); }); - job->start(APPLICATION->network()); + job->start(); m_loadingLogos.append(logo); } @@ -158,7 +158,7 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - NetJob *netJob = new NetJob("Flame::Search"); + NetJob *netJob = new NetJob("Flame::Search", APPLICATION->network()); auto searchUrl = QString( "https://addons-ecs.forgesvc.net/api/v2/addon/search?" "categoryId=0&" @@ -171,7 +171,7 @@ void ListModel::performPaginatedSearch() ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index cb1185f7..1138a298 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -109,7 +109,7 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; - NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name)); + NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); int addonId = current.addonId; netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); @@ -140,7 +140,7 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); }); - netJob->start(APPLICATION->network()); + netJob->start(); } else { 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..37244fed 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" @@ -91,11 +107,11 @@ void ListModel::request() modpacks.clear(); endResetModel(); - auto *netJob = new NetJob("Ftb::Request"); + auto *netJob = new NetJob("Ftb::Request", APPLICATION->network()); auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all"); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); @@ -134,12 +150,11 @@ void ListModel::requestFailed(QString reason) void ListModel::requestPack() { - auto *netJob = new NetJob("Ftb::Search"); - auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1") - .arg(currentPack); + auto *netJob = new NetJob("Ftb::Search", APPLICATION->network()); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1").arg(currentPack); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished); QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed); @@ -255,7 +270,7 @@ void ListModel::requestLogo(QString logo, QString url) bool stale = entry->isStale(); - NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo)); + NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -272,7 +287,7 @@ void ListModel::requestLogo(QString logo, QString url) auto &newLogoEntry = m_logoMap[logo]; newLogoEntry.downloadJob = job; newLogoEntry.fullpath = fullPath; - job->start(APPLICATION->network()); + job->start(); } } 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/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 5fa932b7..9c46e887 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -216,7 +216,7 @@ void ListModel::requestLogo(QString file) } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); - NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file)); + NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); @@ -234,7 +234,7 @@ void ListModel::requestLogo(QString file) emit logoFailed(file); }); - job->start(APPLICATION->network()); + job->start(); m_loadingLogos.append(file); } diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 63c2d4c4..0167f746 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -91,7 +91,7 @@ void Technic::ListModel::searchWithTerm(const QString& term) void Technic::ListModel::performSearch() { - NetJob *netJob = new NetJob("Technic::Search"); + NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { searchUrl = "https://api.technicpack.net/trending?build=multimc"; @@ -104,7 +104,7 @@ void Technic::ListModel::performSearch() } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; - jobPtr->start(APPLICATION->network()); + jobPtr->start(); QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); } @@ -216,7 +216,7 @@ void Technic::ListModel::requestLogo(QString logo, QString url) } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); - NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo)); + NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -231,7 +231,7 @@ void Technic::ListModel::requestLogo(QString logo, QString url) logoFailed(logo); }); - job->start(APPLICATION->network()); + job->start(); m_loadingLogos.append(logo); } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index ac69675c..67f6e52c 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -110,8 +110,8 @@ void TechnicPage::suggestCurrent() metadataLoaded(); return; } - - NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name)); + + NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); QString slug = current.slug; netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); @@ -167,7 +167,7 @@ void TechnicPage::suggestCurrent() current.metadataLoaded = true; metadataLoaded(); }); - netJob->start(APPLICATION->network()); + netJob->start(); } // expects current.metadataLoaded to be true 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/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index b9d7620c..ed07e082 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -319,7 +319,7 @@ void JavaSettingsWidget::on_javaStatusBtn_clicked() } CustomMessageBox::selectable( this, - failed ? QObject::tr("Java test success") : QObject::tr("Java test failure"), + failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), text, failed ? QMessageBox::Critical : QMessageBox::Information )->show(); diff --git a/launcher/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp index eba59142..48fe767a 100644 --- a/launcher/updater/DownloadTask.cpp +++ b/launcher/updater/DownloadTask.cpp @@ -47,7 +47,7 @@ void DownloadTask::loadVersionInfo() { setStatus(tr("Loading version information...")); - NetJob *netJob = new NetJob("Version Info"); + NetJob *netJob = new NetJob("Version Info", m_network); // Find the index URL. QUrl newIndexUrl = QUrl(m_status.newRepoUrl).resolved(QString::number(m_status.newVersionId) + ".json"); @@ -67,7 +67,7 @@ void DownloadTask::loadVersionInfo() connect(netJob, &NetJob::succeeded, this, &DownloadTask::processDownloadedVersionInfo); connect(netJob, &NetJob::failed, this, &DownloadTask::vinfoDownloadFailed); m_vinfoNetJob.reset(netJob); - netJob->start(m_network); + netJob->start(); } void DownloadTask::vinfoDownloadFailed() @@ -121,7 +121,7 @@ void DownloadTask::processDownloadedVersionInfo() setStatus(tr("Processing file lists - figuring out how to install the update...")); // make a new netjob for the actual update files - NetJob::Ptr netJob (new NetJob("Update Files")); + NetJob::Ptr netJob = new NetJob("Update Files", m_network); // fill netJob and operationList if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations)) @@ -145,7 +145,7 @@ void DownloadTask::processDownloadedVersionInfo() } qDebug() << "Begin downloading update files to" << m_updateFilesDir.path(); m_filesNetJob = netJob; - m_filesNetJob->start(m_network); + m_filesNetJob->start(); } void DownloadTask::fileDownloadFinished() diff --git a/launcher/updater/DownloadTask_test.cpp b/launcher/updater/DownloadTask_test.cpp index dc263c17..8e823a63 100644 --- a/launcher/updater/DownloadTask_test.cpp +++ b/launcher/updater/DownloadTask_test.cpp @@ -179,7 +179,8 @@ slots: OperationList operations; - processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy"), operations); + shared_qobject_ptr<QNetworkAccessManager> network = new QNetworkAccessManager(); + processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy", network), operations); qDebug() << (operations == expectedOperations); qDebug() << operations; qDebug() << expectedOperations; diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index c72bbe0b..efdb6093 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -104,11 +104,11 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); - indexJob = new NetJob("GoUpdate Repository Index"); + indexJob = new NetJob("GoUpdate Repository Index", m_network); indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); - indexJob->start(m_network); + indexJob->start(); } void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) @@ -191,11 +191,11 @@ void UpdateChecker::updateChanList(bool notifyNoUpdate) } m_chanListLoading = true; - chanListJob = new NetJob("Update System Channel List"); + chanListJob = new NetJob("Update System Channel List", m_network); chanListJob->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData)); connect(chanListJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); connect(chanListJob.get(), &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); - chanListJob->start(m_network); + chanListJob->start(); } void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate) diff --git a/libraries/iconfix/internal/qiconloader.cpp b/libraries/iconfix/internal/qiconloader.cpp index 41cf3d50..0d8466f0 100644 --- a/libraries/iconfix/internal/qiconloader.cpp +++ b/libraries/iconfix/internal/qiconloader.cpp @@ -320,7 +320,7 @@ Description: Make it so that the QIcon loader honors /usr/share/pixmaps icon theme specification. Bug: https://bugreports.qt.nokia.com/browse/QTBUG-12874 *********************************************************************/ -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) /* Freedesktop standard says to look in /usr/share/pixmaps last */ if (entries.isEmpty()) { diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index dba5a1ae..d0bea2a5 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 1.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..ff2a4149 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 1.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 diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp index fb96c72c..b3098522 100644 --- a/libraries/systeminfo/src/sys_unix.cpp +++ b/libraries/systeminfo/src/sys_unix.cpp @@ -47,6 +47,7 @@ Sys::KernelInfo Sys::getKernelInfo() uint64_t Sys::getSystemRam() { std::string token; +#ifdef Q_OS_LINUX std::ifstream file("/proc/meminfo"); while(file >> token) { @@ -65,6 +66,19 @@ uint64_t Sys::getSystemRam() // ignore rest of the line file.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); } +#elif defined(Q_OS_FREEBSD) + char buff[512]; + FILE *fp = popen("sysctl hw.physmem", "r"); + if (fp != NULL) + { + while(fgets(buff, 512, fp) != NULL) + { + std::string str(buff); + uint64_t mem = std::stoull(str.substr(12, std::string::npos)); + return mem * 1024ull; + } + } +#endif return 0; // nothing found } |