diff --git a/README.md b/README.md index 641622b5c..093bc94da 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe ## Translations -The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at +The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at . ## Building diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index 69e57b4d5..c9962c443 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -89,6 +89,10 @@ function( -Wdouble-promotion # warn if float is implicit promoted to double -Wformat=2 # warn on security issues around functions that format output (ie printf) -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation + # -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results + # in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour + # instead of the exact standard wording so we can safely ignore it + -Wno-gnu-zero-variadic-macro-arguments ) endif() diff --git a/flake.lock b/flake.lock index f96336551..b381db5ae 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1693611461, - "narHash": "sha256-aPODl8vAgGQ0ZYFIRisxYG5MOGSkIczvu2Cd8Gb9+1Y=", + "lastModified": 1696343447, + "narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "7f53fdb7bdc5bb237da7fefef12d099e4fd611ca", + "rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4", "type": "github" }, "original": { @@ -106,11 +106,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1695318763, - "narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=", + "lastModified": 1696661029, + "narHash": "sha256-GIB5VTkvsDIqfMpdtuetOzpm64P8wm8nBSv5Eo8XM3Y=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e12483116b3b51a185a33a272bf351e357ba9a99", + "rev": "2de1be5b51c3d6fa833f1c1f222dc867dd054b31", "type": "github" }, "original": { @@ -123,11 +123,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1693471703, - "narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=", + "lastModified": 1696019113, + "narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85", + "rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a", "type": "github" }, "original": { @@ -153,11 +153,11 @@ ] }, "locked": { - "lastModified": 1694364351, - "narHash": "sha256-oadhSCqopYXxURwIA6/Anpe5IAG11q2LhvTJNP5zE6o=", + "lastModified": 1696516544, + "narHash": "sha256-8rKE8Je6twTNFRTGF63P9mE3lZIq917RAicdc4XJO80=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "4f883a76282bc28eb952570afc3d8a1bf6f481d7", + "rev": "66c352d33e0907239e4a69416334f64af2c685cc", "type": "github" }, "original": { diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 5a26746fc..7f7c8ce4a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -217,13 +217,9 @@ set(MINECRAFT_SOURCES 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/flows/Offline.cpp @@ -237,12 +233,8 @@ set(MINECRAFT_SOURCES 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/MinecraftProfileStepMojang.cpp - minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -251,8 +243,6 @@ set(MINECRAFT_SOURCES 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 @@ -947,8 +937,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.h - ui/dialogs/LoginDialog.cpp - ui/dialogs/LoginDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h ui/dialogs/OfflineLoginDialog.cpp @@ -1107,7 +1095,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui ui/dialogs/AboutDialog.ui - ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index b21eb281f..21a146062 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -88,8 +88,8 @@ void LaunchController::decideAccount() if (accounts->count() <= 0) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Microsoft or Mojang " - "account logged in. Mojang accounts can only be used offline. " + tr("In order to play Minecraft, you must have at least one Microsoft " + "account which owns Minecraft logged in." "Would you like to open the account manager to add an account now?"), QMessageBox::Information, QMessageBox::Yes | QMessageBox::No) ->exec(); diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 30815b5a8..49e9ed06e 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -24,11 +24,11 @@ struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} - virtual QString descriptor() { return id.toString(); } + virtual QString descriptor() override { return id.toString(); } - virtual QString name() { return id.toString(); } + virtual QString name() override { return id.toString(); } - virtual QString typeString() const { return arch; } + virtual QString typeString() const override { return arch; } virtual bool operator<(BaseVersion& a) override; virtual bool operator>(BaseVersion& a) override; diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 3512c3079..cca1ed6d4 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -403,6 +403,14 @@ QList JavaUtils::FindJavaPaths() scanJavaDirs("/opt/jdks"); // flatpak scanJavaDirs("/app/jdk"); + + auto home = qEnvironmentVariable("HOME"); + + // javas downloaded by IntelliJ + scanJavaDirs(FS::PathCombine(home, ".jdks")); + // javas downloaded by sdkman + scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java")); + javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 86ef3b30e..f9833b972 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -856,9 +856,6 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess if (sessionRef.access_token != "0") { addToFilter(sessionRef.access_token, tr("")); } - if (sessionRef.client_token.size()) { - addToFilter(sessionRef.client_token, tr("")); - } addToFilter(sessionRef.uuid, tr("")); return filter; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 9e42c5dd6..9e706ae0a 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -1018,8 +1018,7 @@ std::optional PackProfile::getSupportedModLoaders() // TODO: remove this or add version condition once Quilt drops official Fabric support if (loaders & ModPlatform::Quilt) loaders |= ModPlatform::Fabric; - // TODO: remove this or add version condition once NeoForge drops official Forge support - if (loaders & ModPlatform::NeoForge) + if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge)) loaders |= ModPlatform::Forge; return loaders; } diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 474bf7c6e..e1f1e9b1e 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -278,67 +278,6 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) } // namespace -bool AccountData::resumeStateFromV2(QJsonObject data) -{ - // The JSON object must at least have a username for it to be valid. - if (!data.value("username").isString()) { - qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; - return false; - } - - QString userName = data.value("username").toString(""); - QString clientToken = data.value("clientToken").toString(""); - QString accessToken = data.value("accessToken").toString(""); - - QJsonArray profileArray = data.value("profiles").toArray(); - if (profileArray.size() < 1) { - qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found."; - return false; - } - - struct AccountProfile { - QString id; - QString name; - bool legacy; - }; - - QList profiles; - int currentProfileIndex = 0; - int index = -1; - QString currentProfile = data.value("activeProfile").toString(""); - for (QJsonValue profileVal : profileArray) { - index++; - QJsonObject profileObject = profileVal.toObject(); - QString id = profileObject.value("id").toString(""); - QString name = profileObject.value("name").toString(""); - bool legacy_ = profileObject.value("legacy").toBool(false); - if (id.isEmpty() || name.isEmpty()) { - qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name."; - continue; - } - if (id == currentProfile) { - currentProfileIndex = index; - } - profiles.append({ id, name, legacy_ }); - } - auto& profile = profiles[currentProfileIndex]; - - type = AccountType::Mojang; - legacy = profile.legacy; - - minecraftProfile.id = profile.id; - minecraftProfile.name = profile.name; - minecraftProfile.validity = Katabasis::Validity::Assumed; - - yggdrasilToken.token = accessToken; - yggdrasilToken.extra["clientToken"] = clientToken; - yggdrasilToken.extra["userName"] = userName; - yggdrasilToken.validity = Katabasis::Validity::Assumed; - - validity_ = minecraftProfile.validity; - return true; -} - bool AccountData::resumeStateFromV3(QJsonObject data) { auto typeV = data.value("type"); @@ -349,8 +288,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data) auto typeS = typeV.toString(); if (typeS == "MSA") { type = AccountType::MSA; - } else if (typeS == "Mojang") { - type = AccountType::Mojang; } else if (typeS == "Offline") { type = AccountType::Offline; } else { @@ -358,11 +295,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data) return false; } - if (type == AccountType::Mojang) { - legacy = data.value("legacy").toBool(false); - canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); - } - if (type == AccountType::MSA) { auto clientIDV = data.value("msa-client-id"); if (clientIDV.isString()) { @@ -395,15 +327,7 @@ bool AccountData::resumeStateFromV3(QJsonObject data) QJsonObject AccountData::saveState() const { QJsonObject output; - if (type == AccountType::Mojang) { - output["type"] = "Mojang"; - if (legacy) { - output["legacy"] = true; - } - if (canMigrateToMSA) { - output["canMigrateToMSA"] = true; - } - } else if (type == AccountType::MSA) { + if (type == AccountType::MSA) { output["type"] = "MSA"; output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); @@ -420,51 +344,11 @@ QJsonObject AccountData::saveState() const return output; } -QString AccountData::userName() const -{ - if (type == AccountType::MSA) { - return QString(); - } - return yggdrasilToken.extra["userName"].toString(); -} - QString AccountData::accessToken() const { return yggdrasilToken.token; } -QString AccountData::clientToken() const -{ - if (type != AccountType::Mojang) { - return QString(); - } - return yggdrasilToken.extra["clientToken"].toString(); -} - -void AccountData::setClientToken(QString clientToken) -{ - if (type != AccountType::Mojang) { - return; - } - yggdrasilToken.extra["clientToken"] = clientToken; -} - -void AccountData::generateClientTokenIfMissing() -{ - if (yggdrasilToken.extra.contains("clientToken")) { - return; - } - invalidateClientToken(); -} - -void AccountData::invalidateClientToken() -{ - if (type != AccountType::Mojang) { - return; - } - yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); -} - QString AccountData::profileId() const { return minecraftProfile.id; @@ -482,9 +366,6 @@ QString AccountData::profileName() const QString AccountData::accountDisplayString() const { switch (type) { - case AccountType::Mojang: { - return userName(); - } case AccountType::Offline: { return QObject::tr(""); } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 9b626c34e..bac77e17f 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -71,27 +71,17 @@ struct MinecraftProfile { Katabasis::Validity validity = Katabasis::Validity::None; }; -enum class AccountType { MSA, Mojang, Offline }; +enum class AccountType { MSA, Offline }; enum class AccountState { Unchecked, Offline, Working, Online, Disabled, Errored, Expired, Gone }; struct AccountData { QJsonObject saveState() const; - bool resumeStateFromV2(QJsonObject data); bool resumeStateFromV3(QJsonObject data); //! userName for Mojang accounts, gamertag for MSA QString accountDisplayString() const; - //! Only valid for Mojang accounts. MSA does not preserve this information - QString userName() const; - - //! Only valid for Mojang accounts. - QString clientToken() const; - void setClientToken(QString clientToken); - void invalidateClientToken(); - void generateClientTokenIfMissing(); - //! Yggdrasil access token, as passed to the game. QString accessToken() const; @@ -101,8 +91,6 @@ struct AccountData { QString lastError() const; AccountType type = AccountType::MSA; - bool legacy = false; - bool canMigrateToMSA = false; QString msaClientID; Katabasis::Token msaToken; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 84dbd8416..b141909a9 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -54,7 +54,7 @@ #include -enum AccountListVersion { MojangOnly = 2, MojangMSA = 3 }; +enum AccountListVersion { MojangMSA = 3 }; AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) { @@ -320,17 +320,6 @@ QVariant AccountList::data(const QModelIndex& index, int role) const } } - case MigrationColumn: { - if (account->isMSA() || account->isOffline()) { - return tr("N/A", "Can Migrate"); - } - if (account->canMigrate()) { - return tr("Yes", "Can Migrate"); - } else { - return tr("No", "Can Migrate"); - } - } - default: return QVariant(); } @@ -366,8 +355,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o return tr("Type"); case StatusColumn: return tr("Status"); - case MigrationColumn: - return tr("Can Migrate?"); default: return QVariant(); } @@ -379,11 +366,9 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o case NameColumn: return tr("User name of the account."); case TypeColumn: - return tr("Type of the account - Mojang or MSA."); + return tr("Type of the account (MSA or Offline)"); case StatusColumn: return tr("Current status of the account."); - case MigrationColumn: - return tr("Can this account migrate to a Microsoft account?"); default: return QVariant(); } @@ -473,9 +458,6 @@ bool AccountList::loadList() // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); switch (listVersion) { - case AccountListVersion::MojangOnly: { - return loadV2(root); - } break; case AccountListVersion::MojangMSA: { return loadV3(root); } break; @@ -489,36 +471,6 @@ bool AccountList::loadList() } } -bool AccountList::loadV2(QJsonObject& root) -{ - beginResetModel(); - auto defaultUserName = root.value("activeAccount").toString(""); - QJsonArray accounts = root.value("accounts").toArray(); - for (QJsonValue accountVal : accounts) { - QJsonObject accountObj = accountVal.toObject(); - MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj); - if (account.get() != nullptr) { - auto profileId = account->profileId(); - if (!profileId.size()) { - continue; - } - if (findAccountByProfileId(profileId) != -1) { - continue; - } - connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); - connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); - m_accounts.append(account); - if (defaultUserName.size() && account->mojangUserName() == defaultUserName) { - m_defaultAccount = account; - } - } else { - qWarning() << "Failed to load an account."; - } - } - endResetModel(); - return true; -} - bool AccountList::loadV3(QJsonObject& root) { beginResetModel(); diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 6a0b01916..051d8f958 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -55,7 +55,6 @@ class AccountList : public QAbstractListModel { // TODO: Add icon column. ProfileNameColumn = 0, NameColumn, - MigrationColumn, TypeColumn, StatusColumn, @@ -97,7 +96,6 @@ class AccountList : public QAbstractListModel { void setListFilePath(QString path, bool autosave = false); bool loadList(); - bool loadV2(QJsonObject& root); bool loadV3(QJsonObject& root); bool saveList(); diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 40519476d..074b2d6e3 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -24,10 +24,6 @@ struct AuthSession { GoneOrMigrated } status = Undetermined; - // client token - QString client_token; - // account user name - QString username; // combined session ID QString session; // volatile auth token diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 6c2f0805f..545d06aed 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -51,7 +51,6 @@ #include #include "flows/MSA.h" -#include "flows/Mojang.h" #include "flows/Offline.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) @@ -59,15 +58,6 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); } -MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) -{ - MinecraftAccountPtr account(new MinecraftAccount()); - if (account->data.resumeStateFromV2(json)) { - return account; - } - return nullptr; -} - MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); @@ -77,15 +67,6 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) return nullptr; } -MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString& username) -{ - auto account = makeShared(); - account->data.type = AccountType::Mojang; - account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); - return account; -} - MinecraftAccountPtr MinecraftAccount::createBlankMSA() { MinecraftAccountPtr account(new MinecraftAccount()); @@ -138,18 +119,6 @@ QPixmap MinecraftAccount::getFace() const return skin.scaled(64, 64, Qt::KeepAspectRatio); } -shared_qobject_ptr MinecraftAccount::login(QString password) -{ - Q_ASSERT(m_currentTask.get() == nullptr); - - m_currentTask.reset(new MojangLogin(&data, password)); - connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); - connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); - connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); - emit activityChanged(true); - return m_currentTask; -} - shared_qobject_ptr MinecraftAccount::loginMSA() { Q_ASSERT(m_currentTask.get() == nullptr); @@ -182,10 +151,8 @@ shared_qobject_ptr MinecraftAccount::refresh() if (data.type == AccountType::MSA) { m_currentTask.reset(new MSASilent(&data)); - } else if (data.type == AccountType::Offline) { - m_currentTask.reset(new OfflineRefresh(&data)); } else { - m_currentTask.reset(new MojangRefresh(&data)); + m_currentTask.reset(new OfflineRefresh(&data)); } connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); @@ -296,13 +263,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) } } - // the user name. you have to have an user name - // FIXME: not with MSA - session->username = data.userName(); // volatile auth token session->access_token = data.accessToken(); - // the semi-permanent client token - session->client_token = data.clientToken(); // profile name session->player_name = data.profileName(); // profile ID diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index f04f947fa..910f4a28a 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -85,13 +85,10 @@ class MinecraftAccount : public QObject, public Usable { //! Default constructor explicit MinecraftAccount(QObject* parent = 0); - static MinecraftAccountPtr createFromUsername(const QString& username); - static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createOffline(const QString& username); - static MinecraftAccountPtr loadFromJsonV2(const QJsonObject& json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); static QUuid uuidFromUsername(QString username); @@ -100,12 +97,6 @@ class MinecraftAccount : public QObject, public Usable { QJsonObject saveToJson() const; 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 login(QString password); - shared_qobject_ptr loginMSA(); shared_qobject_ptr loginOffline(); @@ -119,8 +110,6 @@ class MinecraftAccount : public QObject, public Usable { QString accountDisplayString() const { return data.accountDisplayString(); } - QString mojangUserName() const { return data.userName(); } - QString accessToken() const { return data.accessToken(); } QString profileId() const { return data.profileId(); } @@ -129,8 +118,6 @@ class MinecraftAccount : public QObject, public Usable { bool isActive() const; - bool canMigrate() const { return data.canMigrateToMSA; } - bool isMSA() const { return data.type == AccountType::MSA; } bool isOffline() const { return data.type == AccountType::Offline; } @@ -142,12 +129,6 @@ class MinecraftAccount : public QObject, public Usable { QString typeString() const { switch (data.type) { - case AccountType::Mojang: { - if (data.legacy) { - return "legacy"; - } - return "mojang"; - } break; case AccountType::MSA: { return "msa"; } break; diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp deleted file mode 100644 index 97f2a78d4..000000000 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ /dev/null @@ -1,342 +0,0 @@ -/* 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 "Yggdrasil.h" -#include "AccountData.h" - -#include -#include -#include -#include -#include -#include - -#include - -#include "Application.h" - -Yggdrasil::Yggdrasil(AccountData* data, QObject* parent) : AccountTask(data, parent) -{ - changeState(AccountTaskState::STATE_CREATED); -} - -void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) -{ - changeState(AccountTaskState::STATE_WORKING); - - QNetworkRequest netRequest(endpoint); - netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_netReply = APPLICATION->network()->post(netRequest, content); - connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply); - connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers); - connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers); - connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors); - timeout_keeper.setSingleShot(true); - timeout_keeper.start(timeout_max); - counter.setSingleShot(false); - counter.start(time_step); - progress(0, timeout_max); - connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout); - connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat); -} - -void Yggdrasil::executeTask() {} - -void Yggdrasil::refresh() -{ - start(); - /* - * { - * "clientToken": "client identifier" - * "accessToken": "current access token to be refreshed" - * "selectedProfile": // specifying this causes errors - * { - * "id": "profile ID" - * "name": "profile name" - * } - * "requestUser": true/false // request the user structure - * } - */ - QJsonObject req; - req.insert("clientToken", m_data->clientToken()); - req.insert("accessToken", m_data->accessToken()); - /* - { - auto currentProfile = m_account->currentProfile(); - QJsonObject profile; - profile.insert("id", currentProfile->id()); - profile.insert("name", currentProfile->name()); - req.insert("selectedProfile", profile); - } - */ - req.insert("requestUser", false); - QJsonDocument doc(req); - - QUrl reqUrl("https://authserver.mojang.com/refresh"); - QByteArray requestData = doc.toJson(); - - sendRequest(reqUrl, requestData); -} - -void Yggdrasil::login(QString password) -{ - start(); - /* - * { - * "agent": { // optional - * "name": "Minecraft", // So far this is the only encountered value - * "version": 1 // This number might be increased - * // by the vanilla client in the future - * }, - * "username": "mojang account name", // Can be an email address or player name for - * // unmigrated accounts - * "password": "mojang account password", - * "clientToken": "client identifier", // optional - * "requestUser": true/false // request the user structure - * } - */ - QJsonObject req; - - { - QJsonObject agent; - // C++ makes string literals void* for some stupid reason, so we have to tell it - // QString... Thanks Obama. - agent.insert("name", QString("Minecraft")); - agent.insert("version", 1); - req.insert("agent", agent); - } - - req.insert("username", m_data->userName()); - req.insert("password", password); - req.insert("requestUser", false); - - // If we already have a client token, give it to the server. - // Otherwise, let the server give us one. - - m_data->generateClientTokenIfMissing(); - req.insert("clientToken", m_data->clientToken()); - - QJsonDocument doc(req); - - QUrl reqUrl("https://authserver.mojang.com/authenticate"); - QNetworkRequest netRequest(reqUrl); - QByteArray requestData = doc.toJson(); - - sendRequest(reqUrl, requestData); -} - -void Yggdrasil::refreshTimers(qint64, qint64) -{ - timeout_keeper.stop(); - timeout_keeper.start(timeout_max); - progress(count = 0, timeout_max); -} - -void Yggdrasil::heartbeat() -{ - count += time_step; - progress(count, timeout_max); -} - -bool Yggdrasil::abort() -{ - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = Yggdrasil::BY_USER; - m_netReply->abort(); - return true; -} - -void Yggdrasil::abortByTimeout() -{ - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = Yggdrasil::BY_TIMEOUT; - m_netReply->abort(); -} - -void Yggdrasil::sslErrors(QList errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void Yggdrasil::processResponse(QJsonObject responseData) -{ - // Read the response data. We need to get the client token, access token, and the selected - // profile. - qDebug() << "Processing authentication response."; - - // qDebug() << responseData; - // If we already have a client token, make sure the one the server gave us matches our - // existing one. - QString clientToken = responseData.value("clientToken").toString(""); - if (clientToken.isEmpty()) { - // Fail if the server gave us an empty 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(AccountTaskState::STATE_FAILED_HARD, - tr("Authentication server attempted to change the client token. This isn't supported.")); - return; - } - - // Now, we set the access token. - qDebug() << "Getting access token."; - QString accessToken = responseData.value("accessToken").toString(""); - if (accessToken.isEmpty()) { - // Fail if the server didn't give us an access token. - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); - return; - } - // Set the access token. - m_data->yggdrasilToken.token = accessToken; - m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; - m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); - - // Get UUID here since we need it for later - auto profile = responseData.value("selectedProfile"); - if (!profile.isObject()) { - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile.")); - return; - } - - auto profileObj = profile.toObject(); - for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) { - if (i.key() == "name" && i.value().isString()) { - m_data->minecraftProfile.name = i->toString(); - } else if (i.key() == "id" && i.value().isString()) { - m_data->minecraftProfile.id = i->toString(); - } - } - - if (m_data->minecraftProfile.id.isEmpty()) { - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile.")); - return; - } - - // We've made it through the minefield of possible errors. Return true to indicate that - // we've succeeded. - qDebug() << "Finished reading authentication response."; - changeState(AccountTaskState::STATE_SUCCEEDED); -} - -void Yggdrasil::processReply() -{ - changeState(AccountTaskState::STATE_WORKING); - - switch (m_netReply->error()) { - case QNetworkReply::NoError: - break; - case QNetworkReply::TimeoutError: - changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out.")); - return; - case QNetworkReply::OperationCanceledError: - changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); - return; - case QNetworkReply::SslHandshakeFailedError: - changeState(AccountTaskState::STATE_FAILED_SOFT, - tr("SSL Handshake failed.
There might be a few causes for it:
" - "
    " - "
  • You use Windows and need to update your root certificates, please install any outstanding updates.
  • " - "
  • Some device on your network is interfering with SSL traffic. In that case, " - "you have bigger worries than Minecraft not starting.
  • " - "
  • Possibly something else. Check the log file for details
  • " - "
")); - return; - // used for invalid credentials and similar errors. Fall through. - case QNetworkReply::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - break; - case QNetworkReply::ContentGoneError: { - changeState(AccountTaskState::STATE_FAILED_GONE, - tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")); - return; - } - default: - changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)") - .arg(m_netReply->errorString()) - .arg(m_netReply->error())); - return; - } - - // Try to parse the response regardless of the response code. - // Sometimes the auth server will give more information and an error code. - QJsonParseError jsonError; - QByteArray replyData = m_netReply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); - // Check the response code. - int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (responseCode == 200) { - // If the response code was 200, then there shouldn't be an error. Make sure - // anyways. - // Also, sometimes an empty reply indicates success. If there was no data received, - // pass an empty json object to the processResponse function. - if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { - processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); - return; - } else { - changeState(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; - } - return; - } - - // If the response code was not 200, then Yggdrasil may have given us information - // about the error. - // If we can parse the response, then get information from it. Otherwise just say - // there was an unknown error. - if (jsonError.error == QJsonParseError::NoError) { - // We were able to parse the server's response. Woo! - // Call processError. If a subclass has overridden it then they'll handle their - // stuff there. - qDebug() << "The request failed, but the server gave us an error message. Processing error."; - processError(doc.object()); - } else { - // The server didn't say anything regarding the error. Give the user an unknown - // error. - qDebug() << "The request failed and the server gave no error message. Unknown error."; - changeState( - AccountTaskState::STATE_FAILED_SOFT, - tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString())); - } -} - -void Yggdrasil::processError(QJsonObject responseData) -{ - QJsonValue errorVal = responseData.value("error"); - QJsonValue errorMessageValue = responseData.value("errorMessage"); - QJsonValue causeVal = responseData.value("cause"); - - if (errorVal.isString() && errorMessageValue.isString()) { - m_error = std::shared_ptr(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") }); - 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(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); - } -} diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h deleted file mode 100644 index 560d7fb81..000000000 --- a/launcher/minecraft/auth/Yggdrasil.h +++ /dev/null @@ -1,92 +0,0 @@ -/* 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 "AccountTask.h" - -#include -#include -#include -#include - -#include "MinecraftAccount.h" - -class QNetworkAccessManager; -class QNetworkReply; - -/** - * A Yggdrasil task is a task that performs an operation on a given mojang account. - */ -class Yggdrasil : public AccountTask { - Q_OBJECT - public: - explicit Yggdrasil(AccountData* data, QObject* parent = 0); - virtual ~Yggdrasil() = default; - - void refresh(); - void login(QString password); - - struct Error { - QString m_errorMessageShort; - QString m_errorMessageVerbose; - QString m_cause; - }; - std::shared_ptr m_error; - - enum AbortedBy { BY_NOTHING, BY_USER, BY_TIMEOUT } m_aborted = BY_NOTHING; - - protected: - void executeTask() override; - - /** - * Processes the response received from the server. - * If an error occurred, this should emit a failed signal. - * If Yggdrasil gave an error response, it should call setError() first, and then return false. - * Otherwise, it should return true. - * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with - * an empty QJsonObject. - */ - void processResponse(QJsonObject responseData); - - /** - * Processes an error response received from the server. - * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. - * \returns a QString error message that will be passed to emitFailed. - */ - virtual void processError(QJsonObject responseData); - - protected slots: - void processReply(); - void refreshTimers(qint64, qint64); - void heartbeat(); - void sslErrors(QList); - void abortByTimeout(); - - public slots: - virtual bool abort() override; - - private: - void sendRequest(QUrl endpoint, QByteArray content); - - protected: - QNetworkReply* m_netReply = nullptr; - QTimer timeout_keeper; - QTimer counter; - int count = 0; // num msec since time reset - - const int timeout_max = 30000; - const int time_step = 50; -}; diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h index c2c412abc..e39e926dd 100644 --- a/launcher/minecraft/auth/flows/AuthFlow.h +++ b/launcher/minecraft/auth/flows/AuthFlow.h @@ -12,7 +12,6 @@ #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountTask.h" #include "minecraft/auth/AuthStep.h" -#include "minecraft/auth/Yggdrasil.h" class AuthFlow : public AccountTask { Q_OBJECT diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp deleted file mode 100644 index 7e2db16fa..000000000 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "Mojang.h" - -#include "minecraft/auth/steps/GetSkinStep.h" -#include "minecraft/auth/steps/MigrationEligibilityStep.h" -#include "minecraft/auth/steps/MinecraftProfileStepMojang.h" -#include "minecraft/auth/steps/YggdrasilStep.h" - -MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent) -{ - m_steps.append(makeShared(m_data, QString())); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); -} - -MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthFlow(data, parent), m_password(password) -{ - m_steps.append(makeShared(m_data, m_password)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); -} diff --git a/launcher/minecraft/auth/flows/Mojang.h b/launcher/minecraft/auth/flows/Mojang.h deleted file mode 100644 index 779ca7e34..000000000 --- a/launcher/minecraft/auth/flows/Mojang.h +++ /dev/null @@ -1,17 +0,0 @@ -#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/steps/MigrationEligibilityStep.cpp b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp deleted file mode 100644 index 5ce953dfb..000000000 --- a/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "MigrationEligibilityStep.h" - -#include - -#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 headers) -{ - auto requestor = qobject_cast(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 deleted file mode 100644 index 8638975d8..000000000 --- a/launcher/minecraft/auth/steps/MigrationEligibilityStep.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -#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); -}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index 7cdce23f0..a854342bc 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -41,10 +41,6 @@ void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByt qCDebug(authCredentials()) << data; 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; @@ -73,10 +69,5 @@ void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByt 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/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp deleted file mode 100644 index d035e39a0..000000000 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "MinecraftProfileStepMojang.h" - -#include - -#include "Logging.h" -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" -#include "net/NetUtils.h" - -MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {} - -MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; - -QString MinecraftProfileStepMojang::describe() -{ - return tr("Fetching the Minecraft profile."); -} - -void MinecraftProfileStepMojang::perform() -{ - if (m_data->minecraftProfile.id.isEmpty()) { - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); - return; - } - - // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts - QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); - QNetworkRequest req = QNetworkRequest(url); - AuthRequest* request = new AuthRequest(this); - connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); - request->get(req); -} - -void MinecraftProfileStepMojang::rehydrate() -{ - // NOOP, for now. We only save bools and there's nothing to check. -} - -void MinecraftProfileStepMojang::onRequestDone(QNetworkReply::NetworkError error, - QByteArray data, - QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - qCDebug(authCredentials()) << data; - 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); - - if (Net::isApplicationError(error)) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); - } else { - emit finished(AccountTaskState::STATE_OFFLINE, - tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); - } - return; - } - if (!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { - m_data->minecraftProfile = MinecraftProfile(); - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); - return; - } - - 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/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h deleted file mode 100644 index 730ec3f68..000000000 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -#include "QObjectPtr.h" -#include "minecraft/auth/AuthStep.h" - -class MinecraftProfileStepMojang : public AuthStep { - Q_OBJECT - - public: - explicit MinecraftProfileStepMojang(AccountData* data); - virtual ~MinecraftProfileStepMojang() noexcept; - - void perform() override; - void rehydrate() override; - - QString describe() override; - - private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); -}; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index 61c33a18d..856036d23 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -38,7 +38,7 @@ void XboxUserStep::perform() QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); - // set contract-verison header (prevent err 400 bad-request?) + // set contract-version header (prevent err 400 bad-request?) // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders request.setRawHeader("x-xbl-contract-version", "1"); diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp deleted file mode 100644 index fdcaa0d67..000000000 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#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); - connect(m_yggdrasil, &Task::aborted, this, &YggdrasilStep::onAuthFailed); -} - -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 deleted file mode 100644 index ef31f34d5..000000000 --- a/launcher/minecraft/auth/steps/YggdrasilStep.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include - -#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/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 88e9ff2b6..3496da2a0 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -31,6 +31,7 @@ class Mod; class Metadata { public: using ModStruct = Packwiz::V1::Mod; + using ModSide = Packwiz::V1::Side; static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct { diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 80c31e456..60b8879c0 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -330,7 +330,8 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet // When you have a Qt build with assertions turned on, proceeding here will abort the application if (added_set.size() > 0) { - beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1); + beginInsertRows(QModelIndex(), static_cast(m_resources.size()), + static_cast(m_resources.size() + added_set.size() - 1)); for (auto& added : added_set) { auto res = new_resources[added]; diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp index 6a9641de2..2c094f26a 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -22,7 +22,7 @@ #include "ShaderPack.h" -#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" +#include void ShaderPack::setPackFormat(ShaderPackFormat new_format) { @@ -35,3 +35,8 @@ bool ShaderPack::valid() const { return m_pack_format != ShaderPackFormat::INVALID; } + +bool ShaderPack::applyFilter(QRegularExpression filter) const +{ + return valid() && Resource::applyFilter(filter); +} diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index ec0f9404e..d07c124be 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -54,6 +54,7 @@ class ShaderPack : public Resource { void setPackFormat(ShaderPackFormat new_format); bool valid() const override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: mutable QMutex m_data_lock; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index 44ed37a47..186d02139 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -1,6 +1,9 @@ #pragma once #include "ResourceFolderModel.h" +#include "minecraft/mod/ShaderPack.h" +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT @@ -9,4 +12,14 @@ class ShaderPackFolderModel : public ResourceFolderModel { explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {} virtual QString id() const override { return "shaderpacks"; } + + [[nodiscard]] Task* createUpdateTask() override + { + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); + } + + [[nodiscard]] Task* createParseTask(Resource& resource) override + { + return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); + } }; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index bd1fe9401..df8c690af 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -94,7 +94,7 @@ QList GetModDependenciesTask::getDependenciesForVersion for (auto ver_dep : version.dependencies) { if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) continue; - + ver_dep = getOverride(ver_dep, providerName); auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { @@ -127,7 +127,7 @@ QList GetModDependenciesTask::getDependenciesForVersion dep != m_pack_dependencies.end()) // check loaded dependencies continue; - c_dependencies.append(getOverride(ver_dep, providerName)); + c_dependencies.append(ver_dep); } return c_dependencies; } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 9f79ba098..2094df4fc 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -122,7 +122,7 @@ void ModFolderLoadTask::getFromMetadata() auto metadata = Metadata::get(m_index_dir, entry); if (!metadata.isValid()) { - return; + continue; } auto* mod = new Mod(m_mods_dir, metadata); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 61e6a3a98..8bd83d988 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -24,6 +24,7 @@ class CheckUpdateTask : public Task { QString old_hash; QString old_version; QString new_version; + std::optional new_version_type; QString changelog; ModPlatform::ResourceProvider provider; shared_qobject_ptr download; @@ -33,10 +34,18 @@ class CheckUpdateTask : public Task { QString old_h, QString old_v, QString new_v, + std::optional new_v_type, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr t) - : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) + : name(name) + , old_hash(old_h) + , old_version(old_v) + , new_version(new_v) + , new_version_type(new_v_type) + , changelog(changelog) + , provider(p) + , download(t) {} }; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index e8e4a38dd..fc79dff15 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -24,6 +24,40 @@ namespace ModPlatform { +static const QMap s_indexed_version_type_names = { + { "release", IndexedVersionType::VersionType::Release }, + { "beta", IndexedVersionType::VersionType::Beta }, + { "alpha", IndexedVersionType::VersionType::Alpha } +}; + +IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} + +IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) +{ + m_type = type; +} + +IndexedVersionType::IndexedVersionType(const IndexedVersionType& other) +{ + m_type = other.m_type; +} + +IndexedVersionType& IndexedVersionType::operator=(const IndexedVersionType& other) +{ + m_type = other.m_type; + return *this; +} + +const QString IndexedVersionType::toString(const IndexedVersionType::VersionType& type) +{ + return s_indexed_version_type_names.key(type, "unknown"); +} + +IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString& type) +{ + return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown); +} + auto ProviderCapabilities::name(ResourceProvider p) -> const char* { switch (p) { diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7d144176d..72294c399 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -25,6 +25,7 @@ #include #include #include +#include class QIODevice; @@ -58,6 +59,34 @@ struct DonationData { QString url; }; +struct IndexedVersionType { + enum class VersionType { Release = 1, Beta, Alpha, Unknown }; + IndexedVersionType(const QString& type); + IndexedVersionType(const IndexedVersionType::VersionType& type); + IndexedVersionType(const IndexedVersionType& type); + IndexedVersionType() : IndexedVersionType(IndexedVersionType::VersionType::Unknown) {} + static const QString toString(const IndexedVersionType::VersionType& type); + static IndexedVersionType::VersionType enumFromString(const QString& type); + bool isValid() const { return m_type != IndexedVersionType::VersionType::Unknown; } + IndexedVersionType& operator=(const IndexedVersionType& other); + bool operator==(const IndexedVersionType& other) const { return m_type == other.m_type; } + bool operator==(const IndexedVersionType::VersionType& type) const { return m_type == type; } + bool operator!=(const IndexedVersionType& other) const { return m_type != other.m_type; } + bool operator!=(const IndexedVersionType::VersionType& type) const { return m_type != type; } + bool operator<(const IndexedVersionType& other) const { return m_type < other.m_type; } + bool operator<(const IndexedVersionType::VersionType& type) const { return m_type < type; } + bool operator<=(const IndexedVersionType& other) const { return m_type <= other.m_type; } + bool operator<=(const IndexedVersionType::VersionType& type) const { return m_type <= type; } + bool operator>(const IndexedVersionType& other) const { return m_type > other.m_type; } + bool operator>(const IndexedVersionType::VersionType& type) const { return m_type > type; } + bool operator>=(const IndexedVersionType& other) const { return m_type >= other.m_type; } + bool operator>=(const IndexedVersionType::VersionType& type) const { return m_type >= type; } + + QString toString() const { return toString(m_type); } + + IndexedVersionType::VersionType m_type; +}; + struct Dependency { QVariant addonId; DependencyType type; @@ -69,6 +98,7 @@ struct IndexedVersion { QVariant fileId; QString version; QString version_number = {}; + IndexedVersionType version_type; QStringList mcVersion; QString downloadUrl; QString date; @@ -107,6 +137,7 @@ struct IndexedPack { QString logoName; QString logoUrl; QString websiteUrl; + QString side; bool versionsLoaded = false; QVector versions; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 0501dda62..c014863a3 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask() } auto download_task = makeShared(pack, latest_ver, m_mods_folder); - m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version, + m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version, latest_ver.version_type, api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), ModPlatform::ResourceProvider::FLAME, download_task); } diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 494dc2a7e..345883c17 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -139,6 +139,22 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); + ModPlatform::IndexedVersionType::VersionType ver_type; + switch (Json::requireInteger(obj, "releaseType")) { + case 1: + ver_type = ModPlatform::IndexedVersionType::VersionType::Release; + break; + case 2: + ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; + break; + case 3: + ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; + break; + default: + ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; + } + file.version_type = ModPlatform::IndexedVersionType(ver_type); + auto hash_list = Json::ensureArray(obj, "hashes"); for (auto h : hash_list) { auto hash_entry = Json::ensureObject(h); diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 21835a543..71f1e4a2d 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -89,6 +89,22 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) // pick the latest version supported file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); + + ModPlatform::IndexedVersionType::VersionType ver_type; + switch (Json::requireInteger(version, "releaseType")) { + case 1: + ver_type = ModPlatform::IndexedVersionType::VersionType::Release; + break; + case 2: + ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; + break; + case 3: + ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; + break; + default: + ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; + } + file.version_type = ModPlatform::IndexedVersionType(ver_type); file.downloadUrl = Json::ensureString(version, "downloadUrl"); // only add if we have a download URL (third party distribution is enabled) diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index b089b722c..b2a12a67f 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -17,6 +17,7 @@ struct IndexedVersion { int addonId; int fileId; QString version; + ModPlatform::IndexedVersionType version_type; QString mcVersion; QString downloadUrl; }; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 761f622bb..091296751 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -70,16 +70,18 @@ void PackInstallTask::downloadPack() setProgress(1, 4); setAbortable(false); - archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); - + auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); + auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path); + entry->setStale(true); + archivePath = entry->getFullPath(); netJobContainer.reset(new NetJob("Download FTB Pack", m_network)); QString url; if (m_pack.type == PackType::Private) { - url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(archivePath); + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path); } else { - url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(archivePath); + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path); } - netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath)); + netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry)); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index ab07a363c..9b7c53854 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -161,8 +161,8 @@ void ModrinthCheckUpdate::executeTask() auto download_task = makeShared(pack, project_ver, m_mods_folder); - m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog, - ModPlatform::ResourceProvider::MODRINTH, download_task); + m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type, + project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task); } m_deps.append(std::make_shared(pack, project_ver)); } diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index a9ddb0c91..e9e8a3b75 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -25,6 +25,7 @@ #include "Json.h" #include "MMCZip.h" #include "minecraft/PackProfile.h" +#include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); @@ -129,7 +130,8 @@ void ModrinthPackExportTask::collectHashes() QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1); sha1.addData(data); - ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() }; + ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size(), + mod->metadata()->side }; resolvedFiles[relative] = resolvedFile; // nice! we've managed to resolve based on local metadata! @@ -272,22 +274,33 @@ QByteArray ModrinthPackExportTask::generateIndex() QString path = iterator.key(); const ResolvedFile& value = iterator.value(); - if (optionalFiles) { - // detect disabled mod - const QFileInfo pathInfo(path); - if (pathInfo.suffix() == "disabled") { - // rename it - path = pathInfo.dir().filePath(pathInfo.completeBaseName()); - // ...and make it optional - QJsonObject env; - env["client"] = "optional"; - env["server"] = "optional"; - fileOut["env"] = env; - } + QJsonObject env; + + // detect disabled mod + const QFileInfo pathInfo(path); + if (optionalFiles && pathInfo.suffix() == "disabled") { + // rename it + path = pathInfo.dir().filePath(pathInfo.completeBaseName()); + env["client"] = "optional"; + env["server"] = "optional"; + } else { + env["client"] = "required"; + env["server"] = "required"; } + switch (iterator->side) { + case Metadata::ModSide::ClientSide: + env["server"] = "unsupported"; + break; + case Metadata::ModSide::ServerSide: + env["client"] = "unsupported"; + break; + case Metadata::ModSide::UniversalSide: + break; + } + fileOut["env"] = env; fileOut["path"] = path; - fileOut["downloads"] = QJsonArray{ iterator.value().url }; + fileOut["downloads"] = QJsonArray{ iterator->url }; QJsonObject hashes; hashes["sha1"] = value.sha1; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 83540dfac..33c42e817 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -44,6 +44,7 @@ class ModrinthPackExportTask : public Task { struct ResolvedFile { QString sha1, sha512, url; qint64 size; + Metadata::ModSide side; }; static const QStringList PREFIXES; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 107b99006..7d0893261 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -27,6 +27,11 @@ static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; +bool shouldDownloadOnSide(QString side) +{ + return side == "required" || side == "optional"; +} + // https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -53,6 +58,17 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); + auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side")); + auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side")); + + if (server && client) { + pack.side = "both"; + } else if (server) { + pack.side = "server"; + } else if (client) { + pack.side = "client"; + } + // Modrinth can have more data than what's provided by the basic search :) pack.extraDataLoaded = false; } @@ -149,6 +165,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); + file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); + file.changelog = Json::requireString(obj, "changelog"); auto dependencies = Json::ensureArray(obj, "dependencies"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 0d07c6361..a154317fe 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -111,8 +111,9 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) unsortedVersions.append(file); } auto orderSortPredicate = [](const ModpackVersion& a, const ModpackVersion& b) -> bool { + bool a_better_release = a.version_type <= b.version_type; // dates are in RFC 3339 format - return a.date > b.date; + return a.date > b.date && a_better_release; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); @@ -128,6 +129,7 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion file.name = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "version_number"); + file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.changelog = Json::ensureString(obj, "changelog"); file.id = Json::requireString(obj, "id"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index effa1a84a..8e5306774 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -45,6 +45,8 @@ #include #include +#include "modplatform/ModIndex.h" + class MinecraftInstance; namespace Modrinth { @@ -79,6 +81,7 @@ struct ModpackExtra { struct ModpackVersion { QString name; QString version; + ModPlatform::IndexedVersionType version_type; QString changelog; QString id; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 71f66bf3e..e35567f24 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include "FileSystem.h" #include "StringUtils.h" @@ -111,6 +113,7 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedP mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; + mod.side = stringToSide(mod_pack.side); return mod; } @@ -154,38 +157,52 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) FS::ensureFilePathExists(index_file.fileName()); } + toml::table update; + switch (mod.provider) { + case (ModPlatform::ResourceProvider::FLAME): + if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) { + qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); + return; + } + update = toml::table{ + { "file-id", mod.file_id.toInt() }, + { "project-id", mod.project_id.toInt() }, + }; + break; + case (ModPlatform::ResourceProvider::MODRINTH): + if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) { + qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); + return; + } + update = toml::table{ + { "mod-id", mod.mod_id().toString().toStdString() }, + { "version", mod.version().toString().toStdString() }, + }; + break; + } + if (!index_file.open(QIODevice::ReadWrite)) { - qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); + qCritical() << QString("Could not open file %1!").arg(normalized_fname); return; } // Put TOML data into the file QTextStream in_stream(&index_file); - auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); }; - { - addToStream("name", mod.name); - addToStream("filename", mod.filename); - addToStream("side", mod.side); - - in_stream << QString("\n[download]\n"); - addToStream("mode", mod.mode); - addToStream("url", mod.url.toString()); - addToStream("hash-format", mod.hash_format); - addToStream("hash", mod.hash); - - in_stream << QString("\n[update]\n"); - in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); - switch (mod.provider) { - case (ModPlatform::ResourceProvider::FLAME): - in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); - in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); - break; - case (ModPlatform::ResourceProvider::MODRINTH): - addToStream("mod-id", mod.mod_id().toString()); - addToStream("version", mod.version().toString()); - break; - } + auto tbl = toml::table{ { "name", mod.name.toStdString() }, + { "filename", mod.filename.toStdString() }, + { "side", sideToString(mod.side).toStdString() }, + { "download", + toml::table{ + { "mode", mod.mode.toStdString() }, + { "url", mod.url.toString().toStdString() }, + { "hash-format", mod.hash_format.toStdString() }, + { "hash", mod.hash.toStdString() }, + } }, + { "update", toml::table{ { ProviderCaps.name(mod.provider), update } } } }; + std::stringstream ss; + ss << tbl; + in_stream << QString::fromStdString(ss.str()); } index_file.flush(); @@ -258,7 +275,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); - mod.side = stringEntry(table, "side"); + mod.side = stringToSide(stringEntry(table, "side")); } { // [download] info @@ -313,4 +330,28 @@ auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod return {}; } +auto V1::sideToString(Side side) -> QString +{ + switch (side) { + case Side::ClientSide: + return "client"; + case Side::ServerSide: + return "server"; + case Side::UniversalSide: + return "both"; + } + return {}; +} + +auto V1::stringToSide(QString side) -> Side +{ + if (side == "client") + return Side::ClientSide; + if (side == "server") + return Side::ServerSide; + if (side == "both") + return Side::UniversalSide; + return Side::UniversalSide; +} + } // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 7edc18cde..dce198b0e 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -35,12 +35,12 @@ auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool shoul class V1 { public: + enum class Side { ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; struct Mod { QString slug{}; QString name{}; QString filename{}; - // FIXME: make side an enum - QString side{ "both" }; + Side side{ Side::UniversalSide }; // [download] QString mode{}; @@ -93,6 +93,9 @@ class V1 { * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; + + static auto sideToString(Side side) -> QString; + static auto stringToSide(QString side) -> Side; }; } // namespace Packwiz diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 4858e7d46..401f4a19b 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -362,7 +362,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); }); - connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { repopulateAccountsMenu(); }); + connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { defaultAccountChanged(); }); // Show initial account defaultAccountChanged(); @@ -871,7 +871,7 @@ void MainWindow::finalizeInstance(InstancePtr inst) } else { CustomMessageBox::selectable(this, tr("Error"), tr("The launcher cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Microsoft or Mojang account."), + "one account added.\nPlease add a Microsoft account."), QMessageBox::Warning) ->show(); } diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp deleted file mode 100644 index 7296a13ef..000000000 --- a/launcher/ui/dialogs/LoginDialog.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* 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 "LoginDialog.h" -#include "ui_LoginDialog.h" - -#include "minecraft/auth/AccountTask.h" - -#include - -LoginDialog::LoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LoginDialog) -{ - ui->setupUi(this); - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -LoginDialog::~LoginDialog() -{ - delete ui; -} - -// Stage 1: User interaction -void LoginDialog::accept() -{ - setUserInputsEnabled(false); - ui->progressBar->setVisible(true); - - // Setup the login task and start it - m_account = MinecraftAccount::createFromUsername(ui->userTextBox->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); - connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); - m_loginTask->start(); -} - -void LoginDialog::setUserInputsEnabled(bool enable) -{ - ui->userTextBox->setEnabled(enable); - ui->passTextBox->setEnabled(enable); - ui->buttonBox->setEnabled(enable); -} - -// Enable the OK button only when both textboxes contain something. -void LoginDialog::on_userTextBox_textEdited(const QString& newText) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); -} -void LoginDialog::on_passTextBox_textEdited(const QString& newText) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); -} - -void LoginDialog::onTaskFailed(const QString& reason) -{ - // Set message - auto lines = reason.split('\n'); - QString processed; - for (auto line : lines) { - if (line.size()) { - processed += "" + line + "
"; - } else { - processed += "
"; - } - } - ui->label->setText(processed); - - // Re-enable user-interaction - setUserInputsEnabled(true); - ui->progressBar->setVisible(false); -} - -void LoginDialog::onTaskSucceeded() -{ - QDialog::accept(); -} - -void LoginDialog::onTaskStatus(const QString& status) -{ - ui->label->setText(status); -} - -void LoginDialog::onTaskProgress(qint64 current, qint64 total) -{ - ui->progressBar->setMaximum(total); - ui->progressBar->setValue(current); -} - -// Public interface -MinecraftAccountPtr LoginDialog::newAccount(QWidget* parent, QString msg) -{ - LoginDialog dlg(parent); - dlg.ui->label->setText(msg); - if (dlg.exec() == QDialog::Accepted) { - return dlg.m_account; - } - return nullptr; -} diff --git a/launcher/ui/dialogs/LoginDialog.h b/launcher/ui/dialogs/LoginDialog.h deleted file mode 100644 index 601b5fa77..000000000 --- a/launcher/ui/dialogs/LoginDialog.h +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include - -#include "minecraft/auth/MinecraftAccount.h" -#include "tasks/Task.h" - -namespace Ui { -class LoginDialog; -} - -class LoginDialog : public QDialog { - Q_OBJECT - - public: - ~LoginDialog(); - - static MinecraftAccountPtr newAccount(QWidget* parent, QString message); - - private: - explicit LoginDialog(QWidget* parent = 0); - - void setUserInputsEnabled(bool enable); - - protected slots: - void accept(); - - void onTaskFailed(const QString& reason); - void onTaskSucceeded(); - void onTaskStatus(const QString& status); - void onTaskProgress(qint64 current, qint64 total); - - void on_userTextBox_textEdited(const QString& newText); - void on_passTextBox_textEdited(const QString& newText); - - private: - Ui::LoginDialog* ui; - MinecraftAccountPtr m_account; - Task::Ptr m_loginTask; -}; diff --git a/launcher/ui/dialogs/LoginDialog.ui b/launcher/ui/dialogs/LoginDialog.ui deleted file mode 100644 index 8fa4a45d2..000000000 --- a/launcher/ui/dialogs/LoginDialog.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - LoginDialog - - - - 0 - 0 - 421 - 198 - - - - - 0 - 0 - - - - Add Account - - - - - - Message label placeholder. - - - Qt::RichText - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Email - - - - - - - QLineEdit::Password - - - Password - - - - - - - 24 - - - false - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index ee64b392d..1a70ea59a 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -219,8 +219,10 @@ void ModUpdateDialog::checkCandidates() if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); auto download_task = makeShared(dep->pack, dep->version, m_mod_model); - CheckUpdateTask::UpdatableMod updatable = { dep->pack->name, dep->version.hash, "", dep->version.version, - changelog, dep->pack->provider, download_task }; + CheckUpdateTask::UpdatableMod updatable = { + dep->pack->name, dep->version.hash, "", dep->version.version, dep->version.version_type, + changelog, dep->pack->provider, download_task + }; appendMod(updatable, getRequiredBy.value(dep->version.addonId.toString())); m_tasks.insert(updatable.name, updatable.download); @@ -415,6 +417,11 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri auto new_version_item = new QTreeWidgetItem(item_top); new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); + if (info.new_version_type.has_value()) { + auto new_version_type_itme = new QTreeWidgetItem(item_top); + new_version_type_itme->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString())); + } + if (!requiredBy.isEmpty()) { auto requiredByItem = new QTreeWidgetItem(item_top); if (requiredBy.length() == 1) { diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index c40b15469..dc7cfff06 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -167,8 +167,8 @@ void ResourceDownloadDialog::confirm() }); for (auto& task : selected) { confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), - ProviderCaps.name(task->getProvider()), - getRequiredBy.value(task->getPack()->addonId.toString()) }); + ProviderCaps.name(task->getProvider()), getRequiredBy.value(task->getPack()->addonId.toString()), + task->getVersion().version_type.toString() }); } if (confirm_dialog->exec()) { diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 78c2542fd..aa668f8c2 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -77,6 +77,10 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->insertChildren(childIndx++, { requiredByItem }); } + auto versionTypeItem = new QTreeWidgetItem(itemTop); + versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type)); + itemTop->insertChildren(childIndx++, { versionTypeItem }); + ui->modTreeWidget->addTopLevelItem(itemTop); } diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index a520cc2a6..596f39c8e 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -18,6 +18,7 @@ class ReviewMessageBox : public QDialog { QString custom_file_path{}; QString provider; QStringList required_by; + QString version_type; }; void appendResource(ResourceInformation&& info); diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 25ccfd0d7..5e09eb2fd 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -44,7 +44,6 @@ #include #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/OfflineLoginDialog.h" @@ -55,8 +54,7 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new ui->setupUi(this); ui->listView->setEmptyString( tr("Welcome!\n" - "If you're new here, you can select the \"Add Microsoft\" or \"Add Mojang\" buttons to link your Microsoft and/or Mojang " - "accounts.")); + "If you're new here, you can select the \"Add Microsoft\" button to link your Microsoft account.")); ui->listView->setEmptyMode(VersionListView::String); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); @@ -65,7 +63,6 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new ui->listView->setModel(m_accounts.get()); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::MigrationColumn, QHeaderView::ResizeToContents); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents); ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); @@ -130,19 +127,6 @@ void AccountListPage::listChanged() updateButtonStates(); } -void AccountListPage::on_actionAddMojang_triggered() -{ - MinecraftAccountPtr account = - LoginDialog::newAccount(this, tr("Please enter your Mojang account email and password to add your account.")); - - if (account) { - m_accounts->addAccount(account); - if (m_accounts->count() == 1) { - m_accounts->setDefaultAccount(account); - } - } -} - void AccountListPage::on_actionAddMicrosoft_triggered() { MinecraftAccountPtr account = @@ -160,7 +144,7 @@ void AccountListPage::on_actionAddOffline_triggered() { if (!m_accounts->anyAccountIsValid()) { QMessageBox::warning(this, tr("Error"), - tr("You must add a Microsoft or Mojang account that owns Minecraft before you can add an offline account." + tr("You must add a Microsoft account that owns Minecraft before you can add an offline account." "

" "If you have lost your account you can contact Microsoft for support.")); return; diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 64702cff7..dd49a95d6 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -70,7 +70,6 @@ class AccountListPage : public QMainWindow, public BasePage { void retranslate() override; public slots: - void on_actionAddMojang_triggered(); void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); void on_actionRemove_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 0e73f8c52..c9b770ab2 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -53,7 +53,6 @@ false - @@ -62,11 +61,6 @@ - - - Add &Mojang - - &Set Default diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 8fdaf065d..26a8302db 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -8,6 +8,7 @@ #include #include "ui_ManagedPackPage.h" +#include #include #include #include @@ -223,6 +224,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); + connect(ui->updateFromFileButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::updateFromFile); } // MODRINTH @@ -350,6 +352,27 @@ void ModrinthManagedPackPage::update() m_instance_window->close(); } +void ModrinthManagedPackPage::updateFromFile() +{ + auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "Modrinth pack (*.mrpack *.zip)"); + QMap extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString()); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + // FLAME FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) @@ -358,6 +381,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); + connect(ui->updateFromFileButton, &QPushButton::clicked, this, &FlameManagedPackPage::updateFromFile); } void FlameManagedPackPage::parseManagedPack() @@ -492,4 +516,25 @@ void FlameManagedPackPage::update() m_instance_window->close(); } +void FlameManagedPackPage::updateFromFile() +{ + auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "CurseForge pack (*.zip)"); + + QMap extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString()); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} #include "ManagedPackPage.moc" diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index 1ac6fc038..d77cb97b8 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -65,6 +65,7 @@ class ManagedPackPage : public QWidget, public BasePage { virtual void suggestVersion(); virtual void update(){}; + virtual void updateFromFile(){}; protected slots: /** Does the necessary UI changes for when something failed. @@ -123,6 +124,7 @@ class ModrinthManagedPackPage final : public ManagedPackPage { void suggestVersion() override; void update() override; + void updateFromFile() override; private: NetJob::Ptr m_fetch_job = nullptr; @@ -145,6 +147,7 @@ class FlameManagedPackPage final : public ManagedPackPage { void suggestVersion() override; void update() override; + void updateFromFile() override; private: NetJob::Ptr m_fetch_job = nullptr; diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui index 05e91bbca..54ff08e94 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.ui +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -153,6 +153,19 @@ + + + + + 0 + 0 + + + + Update from file + + + diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 69c34dafe..625d37933 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -206,6 +206,14 @@ void ModFolderPage::installMods() void ModFolderPage::updateMods() { + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (!profile->getModLoaders().has_value()) { + QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); + return; + } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedMods(selection); diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index e22c764ca..2918261d2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -411,7 +411,7 @@ void VersionPage::on_actionDownload_All_triggered() if (!APPLICATION->accounts()->anyAccountIsValid()) { CustomMessageBox::selectable(this, tr("Error"), tr("Cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Microsoft or Mojang account."), + "one account added.\nPlease add a Microsoft account."), QMessageBox::Warning) ->show(); return; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 7eb08f6a2..d6cc1fdcc 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -131,8 +131,10 @@ void ModPage::updateVersionList() } // Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out - if ((valid || m_filter->versions.empty()) && !optedOut(version)) - m_ui->versionSelectionBox->addItem(version.version, QVariant(i)); + if ((valid || m_filter->versions.empty()) && !optedOut(version)) { + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; + m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i)); + } } if (m_ui->versionSelectionBox->count() == 0) { m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index fc7d64a48..44a91003d 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -266,6 +266,9 @@ void ResourcePage::updateVersionList() if (optedOut(version)) continue; + auto release_type = current_pack->versions[i].version_type.isValid() + ? QString(" [%1]").arg(current_pack->versions[i].version_type.toString()) + : ""; m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i)); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 50656f427..584d94ad9 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -176,7 +176,8 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde } for (auto version : current.versions) { - ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; + ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(version.downloadUrl)); } QVariant current_updated; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f7fa8fd78..8e2b9a902 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -217,12 +217,13 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI qDebug() << *response; qWarning() << "Error while reading modrinth modpack version: " << e.cause(); } - for (auto version : current.versions) { + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; if (!version.name.contains(version.version)) - ui->versionSelectionBox->addItem(QString("%1 — %2").arg(version.name, version.version), QVariant(version.id)); + ui->versionSelectionBox->addItem(QString("%1 — %2%3").arg(version.name, version.version, release_type), + QVariant(version.id)); else - ui->versionSelectionBox->addItem(version.name, QVariant(version.id)); + ui->versionSelectionBox->addItem(QString("%1%2").arg(version.name, release_type), QVariant(version.id)); } QVariant current_updated; diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index 99729fbdf..d5ef1cc9f 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -82,7 +82,7 @@ class VersionSelectWidget : public QWidget { void selectedVersionChanged(BaseVersion::Ptr version); protected: - virtual void closeEvent(QCloseEvent*); + virtual void closeEvent(QCloseEvent*) override; private slots: void onTaskSucceeded(); diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix index 8bc255e71..cd356c8d7 100644 --- a/nix/pkg/wrapper.nix +++ b/nix/pkg/wrapper.nix @@ -4,6 +4,7 @@ symlinkJoin, prismlauncher-unwrapped, wrapQtAppsHook, + addOpenGLRunpath, qtbase, # needed for wrapQtAppsHook qtsvg, qtwayland, @@ -85,7 +86,7 @@ in in ["--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}"] ++ lib.optionals stdenv.isLinux [ - "--set LD_LIBRARY_PATH /run/opengl-driver/lib:${lib.makeLibraryPath runtimeLibs}" + "--set LD_LIBRARY_PATH ${addOpenGLRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 "--prefix PATH : ${lib.makeBinPath runtimePrograms}" ]; diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp index d1b274d12..e4abda9f9 100644 --- a/tests/Packwiz_test.cpp +++ b/tests/Packwiz_test.cpp @@ -42,7 +42,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Borderless Mining"); QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); - QCOMPARE(metadata.side, "client"); + QCOMPARE(metadata.side, Packwiz::V1::Side::ClientSide); QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); QCOMPARE(metadata.hash_format, "sha512"); @@ -72,7 +72,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); - QCOMPARE(metadata.side, "both"); + QCOMPARE(metadata.side, Packwiz::V1::Side::UniversalSide); QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); QCOMPARE(metadata.hash_format, "murmur2"); diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index d25bf7bb5..4c67cc544 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -55,6 +55,8 @@ class VersionTest : public QObject { << "2.2.0" << true << false; QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false; + QTest::newRow("lessThan, snapshot") << "1.20.0-rc2" + << "1.20.1" << true << false; QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false; @@ -72,6 +74,8 @@ class VersionTest : public QObject { << "1.2" << false << false; QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; + QTest::newRow("greaterThan, snapshot") << "1.20.2-rc2" + << "1.20.1" << false << false; } private slots: