diff options
Diffstat (limited to 'launcher/minecraft')
56 files changed, 952 insertions, 623 deletions
diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index c01733b6..1c65a212 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -284,7 +284,7 @@ bool reconstructAssets(QString assetsId, QString resourcesFolder) } -NetActionPtr AssetObject::getDownloadAction() +NetAction::Ptr AssetObject::getDownloadAction() { QFileInfo objectFile(getLocalPath()); if ((!objectFile.isFile()) || (objectFile.size() != size)) @@ -316,7 +316,7 @@ QString AssetObject::getRelPath() return hash.left(2) + "/" + hash; } -NetJobPtr AssetsIndex::getDownloadJob() +NetJob::Ptr AssetsIndex::getDownloadJob() { auto job = new NetJob(QObject::tr("Assets for %1").arg(id)); for (auto &object : objects.values()) diff --git a/launcher/minecraft/AssetsUtils.h b/launcher/minecraft/AssetsUtils.h index 32e57060..3dbf19ed 100644 --- a/launcher/minecraft/AssetsUtils.h +++ b/launcher/minecraft/AssetsUtils.h @@ -25,7 +25,7 @@ struct AssetObject QString getRelPath(); QUrl getUrl(); QString getLocalPath(); - NetActionPtr getDownloadAction(); + NetAction::Ptr getDownloadAction(); QString hash; qint64 size; @@ -33,7 +33,7 @@ struct AssetObject struct AssetsIndex { - NetJobPtr getDownloadJob(); + NetJob::Ptr getDownloadJob(); QString id; QMap<QString, AssetObject> objects; diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 92821065..c7dd5e36 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -1,14 +1,16 @@ #include <meta/VersionList.h> #include <meta/Index.h> -#include <Env.h> #include "Component.h" +#include <QSaveFile> + #include "meta/Version.h" #include "VersionFile.h" #include "minecraft/PackProfile.h" -#include <FileSystem.h> -#include <QSaveFile> +#include "FileSystem.h" #include "OneSixVersionFormat.h" +#include "Application.h" + #include <assert.h> Component::Component(PackProfile * parent, const QString& uid) @@ -85,9 +87,9 @@ std::shared_ptr<class VersionFile> Component::getVersionFile() const std::shared_ptr<class Meta::VersionList> Component::getVersionList() const { // FIXME: what if the metadata index isn't loaded yet? - if(ENV.metadataIndex()->hasUid(m_uid)) + if(APPLICATION->metadataIndex()->hasUid(m_uid)) { - return ENV.metadataIndex()->get(m_uid); + return APPLICATION->metadataIndex()->get(m_uid); } return nullptr; } @@ -192,7 +194,7 @@ bool Component::isRevertible() { if (isCustom()) { - if(ENV.metadataIndex()->hasUid(m_uid)) + if(APPLICATION->metadataIndex()->hasUid(m_uid)) { return true; } @@ -266,7 +268,7 @@ void Component::setVersion(const QString& version) // we don't have a file, therefore we are loaded with metadata m_cachedVersion = version; // see if the meta version is loaded - auto metaVersion = ENV.metadataIndex()->get(m_uid, version); + auto metaVersion = APPLICATION->metadataIndex()->get(m_uid, version); if(metaVersion->isLoaded()) { // if yes, we can continue with that. @@ -350,7 +352,7 @@ bool Component::revert() m_file.reset(); // check local cache for metadata... - auto version = ENV.metadataIndex()->get(m_uid, m_version); + auto version = APPLICATION->metadataIndex()->get(m_uid, m_version); if(version->isLoaded()) { m_metaVersion = version; diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 241d9a49..8bc05a1b 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -3,16 +3,17 @@ #include "PackProfile_p.h" #include "PackProfile.h" #include "Component.h" -#include <Env.h> -#include <meta/Index.h> -#include <meta/VersionList.h> -#include <meta/Version.h> +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "meta/Version.h" #include "ComponentUpdateTask_p.h" -#include <cassert> -#include <Version.h> +#include "cassert" +#include "Version.h" #include "net/Mode.h" #include "OneSixVersionFormat.h" +#include "Application.h" + /* * This is responsible for loading the components of a component list AND resolving dependency issues between them */ @@ -68,7 +69,7 @@ LoadResult composeLoadResult(LoadResult a, LoadResult b) return a; } -static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) +static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { @@ -102,7 +103,7 @@ static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task> } else { - auto metaVersion = ENV.metadataIndex()->get(component->m_uid, component->m_version); + auto metaVersion = APPLICATION->metadataIndex()->get(component->m_uid, component->m_version); component->m_metaVersion = metaVersion; if(metaVersion->isLoaded()) { @@ -126,7 +127,7 @@ static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task> // FIXME: dead code. determine if this can still be useful? /* -static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) +static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { @@ -135,7 +136,7 @@ static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr<Tas } LoadResult result = LoadResult::Failed; - auto metaList = ENV.metadataIndex()->get(component->m_uid); + auto metaList = APPLICATION->metadataIndex()->get(component->m_uid); if(metaList->isLoaded()) { component->m_loaded = true; @@ -151,16 +152,16 @@ static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr<Tas } */ -static LoadResult loadIndex(shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) +static LoadResult loadIndex(Task::Ptr& loadTask, Net::Mode netmode) { // FIXME: DECIDE. do we want to run the update task anyway? - if(ENV.metadataIndex()->isLoaded()) + if(APPLICATION->metadataIndex()->isLoaded()) { qDebug() << "Index is already loaded"; return LoadResult::LoadedLocal; } - ENV.metadataIndex()->load(netmode); - loadTask = ENV.metadataIndex()->getCurrentTask(); + APPLICATION->metadataIndex()->load(netmode); + loadTask = APPLICATION->metadataIndex()->getCurrentTask(); if(loadTask) { return LoadResult::RequiresRemote; @@ -179,7 +180,7 @@ void ComponentUpdateTask::loadComponents() // load the main index (it is needed to determine if components can revert) { // FIXME: tear out as a method? or lambda? - shared_qobject_ptr<Task> indexLoadTask; + Task::Ptr indexLoadTask; auto singleResult = loadIndex(indexLoadTask, d->netmode); result = composeLoadResult(result, singleResult); if(indexLoadTask) @@ -202,7 +203,7 @@ void ComponentUpdateTask::loadComponents() // load all the components OR their lists... for (auto component: d->m_list->d->components) { - shared_qobject_ptr<Task> loadTask; + Task::Ptr loadTask; LoadResult singleResult; RemoteLoadStatus::Type loadType; // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, ignore all that... diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index f2293679..c7982705 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -3,7 +3,6 @@ #include <net/Download.h> #include <net/ChecksumValidator.h> -#include <Env.h> #include <FileSystem.h> #include <BuildConfig.h> @@ -45,14 +44,14 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na } } -QList< std::shared_ptr< NetAction > > Library::getDownloads( +QList<NetAction::Ptr> Library::getDownloads( OpSys system, class HttpMetaCache* cache, QStringList& failedLocalFiles, const QString & overridePath ) const { - QList<NetActionPtr> out; + QList<NetAction::Ptr> out; bool stale = isAlwaysStale(); bool local = isLocal(); diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h index 119b4a86..41d41a8b 100644 --- a/launcher/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -152,7 +152,7 @@ public: /* methods */ bool isForge() const; // Get a list of downloads for this library - QList<NetActionPtr> getDownloads(OpSys system, class HttpMetaCache * cache, + QList<NetAction::Ptr> getDownloads(OpSys system, class HttpMetaCache * cache, QStringList & failedLocalFiles, const QString & overridePath) const; private: /* methods */ diff --git a/launcher/minecraft/Library_test.cpp b/launcher/minecraft/Library_test.cpp index 75bb4db1..47531ad6 100644 --- a/launcher/minecraft/Library_test.cpp +++ b/launcher/minecraft/Library_test.cpp @@ -55,7 +55,7 @@ slots: auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 1); QCOMPARE(failedFiles, {}); - NetActionPtr dl = downloads[0]; + NetAction::Ptr dl = downloads[0]; QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar")); } void test_legacy_url_local_broken() diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 2982a340..4c16e572 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1,15 +1,16 @@ #include "MinecraftInstance.h" -#include <minecraft/launch/CreateGameFolders.h> -#include <minecraft/launch/ExtractNatives.h> -#include <minecraft/launch/PrintInstanceInfo.h> -#include <settings/Setting.h> +#include "minecraft/launch/CreateGameFolders.h" +#include "minecraft/launch/ExtractNatives.h" +#include "minecraft/launch/PrintInstanceInfo.h" +#include "settings/Setting.h" #include "settings/SettingsObject.h" -#include "Env.h" -#include <MMCStrings.h> -#include <pathmatcher/RegexpMatcher.h> -#include <pathmatcher/MultiMatcher.h> -#include <FileSystem.h> -#include <java/JavaVersion.h> +#include "Application.h" + +#include "MMCStrings.h" +#include "pathmatcher/RegexpMatcher.h" +#include "pathmatcher/MultiMatcher.h" +#include "FileSystem.h" +#include "java/JavaVersion.h" #include "MMCTime.h" #include "launch/LaunchTask.h" @@ -18,6 +19,8 @@ #include "launch/steps/Update.h" #include "launch/steps/PreLaunchCommand.h" #include "launch/steps/TextPrint.h" +#include "launch/steps/CheckJava.h" + #include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/DirectJavaLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" @@ -25,25 +28,26 @@ #include "minecraft/launch/ReconstructAssets.h" #include "minecraft/launch/ScanModFolders.h" #include "minecraft/launch/VerifyJavaInstall.h" -#include "java/launch/CheckJava.h" + #include "java/JavaUtils.h" + #include "meta/Index.h" #include "meta/VersionList.h" +#include "icons/IconList.h" + #include "mod/ModFolderModel.h" #include "mod/ResourcePackFolderModel.h" #include "mod/TexturePackFolderModel.h" -#include "WorldList.h" -#include "icons/IIconList.h" +#include "WorldList.h" -#include <QCoreApplication> #include "PackProfile.h" #include "AssetsUtils.h" #include "MinecraftUpdate.h" #include "MinecraftLoadAndCheck.h" -#include <minecraft/gameoptions/GameOptions.h> -#include <minecraft/update/FoldersTask.h> +#include "minecraft/gameoptions/GameOptions.h" +#include "minecraft/update/FoldersTask.h" #define IBUS "@im=ibus" @@ -794,17 +798,17 @@ QString MinecraftInstance::getStatusbarDescription() return description; } -shared_qobject_ptr<Task> MinecraftInstance::createUpdateTask(Net::Mode mode) +Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode) { switch (mode) { case Net::Mode::Offline: { - return shared_qobject_ptr<Task>(new MinecraftLoadAndCheck(this)); + return Task::Ptr(new MinecraftLoadAndCheck(this)); } case Net::Mode::Online: { - return shared_qobject_ptr<Task>(new MinecraftUpdate(this)); + return Task::Ptr(new MinecraftUpdate(this)); } } return nullptr; @@ -816,7 +820,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this())); auto pptr = process.get(); - ENV.icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); + APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index b11270e6..bb45f37b 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -77,7 +77,7 @@ public: std::shared_ptr<GameOptions> gameOptionsModel() const; ////// Launch stuff ////// - shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override; + Task::Ptr createUpdateTask(Net::Mode mode) override; shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; QStringList extraArguments() const override; QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h index 3435b52b..bfeae46b 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -41,7 +41,7 @@ private slots: private: MinecraftInstance *m_inst = nullptr; - shared_qobject_ptr<Task> m_task; + Task::Ptr m_task; QString m_preFailure; QString m_fail_reason; }; diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp index 8f1565b0..32e9cbb6 100644 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ b/launcher/minecraft/MinecraftUpdate.cpp @@ -13,7 +13,6 @@ * limitations under the License. */ -#include "Env.h" #include "MinecraftUpdate.h" #include "MinecraftInstance.h" diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index f6918116..59a8f133 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -20,22 +20,23 @@ #include <QJsonDocument> #include <QJsonArray> #include <QDebug> - -#include "Exception.h" -#include <minecraft/OneSixVersionFormat.h> -#include <FileSystem.h> #include <QSaveFile> -#include <Env.h> -#include <meta/Index.h> -#include <minecraft/MinecraftInstance.h> #include <QUuid> #include <QTimer> -#include <Json.h> + +#include "Exception.h" +#include "minecraft/OneSixVersionFormat.h" +#include "FileSystem.h" +#include "meta/Index.h" +#include "minecraft/MinecraftInstance.h" +#include "Json.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "ComponentUpdateTask.h" +#include "Application.h" + PackProfile::PackProfile(MinecraftInstance * instance) : QAbstractListModel() { @@ -339,7 +340,7 @@ void PackProfile::reload(Net::Mode netmode) } } -shared_qobject_ptr<Task> PackProfile::getCurrentTask() +Task::Ptr PackProfile::getCurrentTask() { return d->m_updateTask; } @@ -481,7 +482,7 @@ bool PackProfile::migratePreComponentConfig() } else if(!intendedVersion.isEmpty()) { - auto metaVersion = ENV.metadataIndex()->get(uid, intendedVersion); + auto metaVersion = APPLICATION->metadataIndex()->get(uid, intendedVersion); component = new Component(this, metaVersion); } else @@ -546,7 +547,7 @@ bool PackProfile::migratePreComponentConfig() auto patchVersion = d->getOldConfigVersion(uid); if(!patchVersion.isEmpty() && !loadedComponents.contains(uid)) { - auto patch = new Component(this, ENV.metadataIndex()->get(uid, patchVersion)); + auto patch = new Component(this, APPLICATION->metadataIndex()->get(uid, patchVersion)); patch->setOrder(order); loadedComponents[uid] = patch; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 3d6cc6c3..f30deb5a 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -85,7 +85,7 @@ public: void resolve(Net::Mode netmode); /// get current running task... - shared_qobject_ptr<Task> getCurrentTask(); + Task::Ptr getCurrentTask(); std::shared_ptr<LaunchProfile> getProfile() const; diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h index 6cd2a4e5..fce921bb 100644 --- a/launcher/minecraft/PackProfile_p.h +++ b/launcher/minecraft/PackProfile_p.h @@ -35,7 +35,7 @@ struct PackProfileData ComponentIndex componentIndex; bool dirty = false; QTimer m_saveTimer; - shared_qobject_ptr<Task> m_updateTask; + Task::Ptr m_updateTask; bool loaded = false; bool interactionDisabled = true; }; diff --git a/launcher/minecraft/VersionFilterData.cpp b/launcher/minecraft/VersionFilterData.cpp index 38e7b60c..c286d266 100644 --- a/launcher/minecraft/VersionFilterData.cpp +++ b/launcher/minecraft/VersionFilterData.cpp @@ -68,4 +68,5 @@ VersionFilterData::VersionFilterData() java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); + java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); } diff --git a/launcher/minecraft/VersionFilterData.h b/launcher/minecraft/VersionFilterData.h index 79756c3f..13445a51 100644 --- a/launcher/minecraft/VersionFilterData.h +++ b/launcher/minecraft/VersionFilterData.h @@ -25,5 +25,7 @@ struct VersionFilterData QDateTime java8BeginsDate; // release data of first version to require Java 16 (21w19a) QDateTime java16BeginsDate; + // release data of first version to require Java 17 (1.18 Pre Release 2) + QDateTime java17BeginsDate; }; extern VersionFilterData g_VersionFilterData; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 5c6de9df..8aa4e37f 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -207,6 +207,35 @@ MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * token return out; } +void entitlementToJSONV3(QJsonObject &parent, MinecraftEntitlement p) { + if(p.validity == Katabasis::Validity::None) { + return; + } + QJsonObject out; + out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); + out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); + parent["entitlement"] = out; +} + +bool entitlementFromJSONV3(const QJsonObject &parent, MinecraftEntitlement & out) { + auto entitlementObject = parent.value("entitlement").toObject(); + if(entitlementObject.isEmpty()) { + return false; + } + { + auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); + auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); + if(!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) { + qWarning() << "mandatory attributes are missing or of unexpected type"; + return false; + } + out.canPlayMinecraft = canPlayMinecraftV.toBool(false); + out.ownsMinecraft = ownsMinecraftV.toBool(false); + out.validity = Katabasis::Validity::Assumed; + } + return true; +} + } bool AccountData::resumeStateFromV2(QJsonObject data) { @@ -304,9 +333,15 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { yggdrasilToken = tokenFromJSONV3(data, "ygg"); minecraftProfile = profileFromJSONV3(data, "profile"); + if(!entitlementFromJSONV3(data, minecraftEntitlement)) { + if(minecraftProfile.validity != Katabasis::Validity::None) { + minecraftEntitlement.canPlayMinecraft = true; + minecraftEntitlement.ownsMinecraft = true; + minecraftEntitlement.validity = Katabasis::Validity::Assumed; + } + } validity_ = minecraftProfile.validity; - return true; } @@ -331,6 +366,7 @@ QJsonObject AccountData::saveState() const { tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); + entitlementToJSONV3(output, minecraftEntitlement); return output; } @@ -378,7 +414,12 @@ QString AccountData::profileId() const { } QString AccountData::profileName() const { - return minecraftProfile.name; + if(minecraftProfile.name.size() == 0) { + return QObject::tr("No profile (%1)").arg(accountDisplayString()); + } + else { + return minecraftProfile.name; + } } QString AccountData::accountDisplayString() const { diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index cf58fb76..09cd2c73 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -21,6 +21,12 @@ struct Cape { QByteArray data; }; +struct MinecraftEntitlement { + bool ownsMinecraft = false; + bool canPlayMinecraft = false; + Katabasis::Validity validity = Katabasis::Validity::None; +}; + struct MinecraftProfile { QString id; QString name; @@ -69,5 +75,6 @@ struct AccountData { Katabasis::Token yggdrasilToken; MinecraftProfile minecraftProfile; + MinecraftEntitlement minecraftEntitlement; Katabasis::Validity validity_ = Katabasis::Validity::None; }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index a76cac55..d7537345 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -37,6 +37,8 @@ enum AccountListVersion { AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { } +AccountList::~AccountList() noexcept {} + int AccountList::findAccountByProfileId(const QString& profileId) const { for (int i = 0; i < count(); i++) { MinecraftAccountPtr account = at(i); @@ -62,28 +64,41 @@ const MinecraftAccountPtr AccountList::at(int i) const return MinecraftAccountPtr(m_accounts.at(i)); } +QStringList AccountList::profileNames() const { + QStringList out; + for(auto & account: m_accounts) { + auto profileName = account->profileName(); + if(profileName.isEmpty()) { + continue; + } + out.append(profileName); + } + return out; +} + void AccountList::addAccount(const MinecraftAccountPtr account) { - // We only ever want accounts with valid profiles. - // Keeping profile-less accounts is pointless and serves no purpose. auto profileId = account->profileId(); - if(!profileId.size()) { - return; - } - - // override/replace existing account with the same profileId - auto existingAccount = findAccountByProfileId(profileId); - if(existingAccount != -1) { - m_accounts[existingAccount] = account; - emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); - onListChanged(); - return; + if(profileId.size()) { + // override/replace existing account with the same profileId + auto existingAccount = findAccountByProfileId(profileId); + if(existingAccount != -1) { + MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; + m_accounts[existingAccount] = account; + if(m_defaultAccount == existingAccountPtr) { + m_defaultAccount = account; + } + emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); + onListChanged(); + return; + } } - // if we don't have this porfileId yet, add the account to the end + // if we don't have this profileId yet, add the account to the end int row = m_accounts.count(); beginInsertRows(QModelIndex(), row, row); - connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); m_accounts.append(account); endInsertRows(); onListChanged(); @@ -95,10 +110,10 @@ void AccountList::removeAccount(QModelIndex index) if(index.isValid() && row >= 0 && row < m_accounts.size()) { auto & account = m_accounts[row]; - if(account == m_activeAccount) + if(account == m_defaultAccount) { - m_activeAccount = nullptr; - onActiveChanged(); + m_defaultAccount = nullptr; + onDefaultAccountChanged(); } beginRemoveRows(QModelIndex(), row, row); m_accounts.removeAt(index.row()); @@ -107,54 +122,54 @@ void AccountList::removeAccount(QModelIndex index) } } -MinecraftAccountPtr AccountList::activeAccount() const +MinecraftAccountPtr AccountList::defaultAccount() const { - return m_activeAccount; + return m_defaultAccount; } -void AccountList::setActiveAccount(const QString &profileId) +void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount) { - if (profileId.isEmpty() && m_activeAccount) + if (!newAccount && m_defaultAccount) { int idx = 0; - auto prevActiveAcc = m_activeAccount; - m_activeAccount = nullptr; + auto previousDefaultAccount = m_defaultAccount; + m_defaultAccount = nullptr; for (MinecraftAccountPtr account : m_accounts) { - if (account == prevActiveAcc) + if (account == previousDefaultAccount) { - emit dataChanged(index(idx), index(idx)); + emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1)); } idx ++; } - onActiveChanged(); + onDefaultAccountChanged(); } else { - auto currentActiveAccount = m_activeAccount; - int currentActiveAccountIdx = -1; - auto newActiveAccount = m_activeAccount; - int newActiveAccountIdx = -1; + auto currentDefaultAccount = m_defaultAccount; + int currentDefaultAccountIdx = -1; + auto newDefaultAccount = m_defaultAccount; + int newDefaultAccountIdx = -1; int idx = 0; for (MinecraftAccountPtr account : m_accounts) { - if (account->profileId() == profileId) + if (account == newAccount) { - newActiveAccount = account; - newActiveAccountIdx = idx; + newDefaultAccount = account; + newDefaultAccountIdx = idx; } - if(currentActiveAccount == account) + if(currentDefaultAccount == account) { - currentActiveAccountIdx = idx; + currentDefaultAccountIdx = idx; } idx++; } - if(currentActiveAccount != newActiveAccount) + if(currentDefaultAccount != newDefaultAccount) { - emit dataChanged(index(currentActiveAccountIdx), index(currentActiveAccountIdx)); - emit dataChanged(index(newActiveAccountIdx), index(newActiveAccountIdx)); - m_activeAccount = newActiveAccount; - onActiveChanged(); + emit dataChanged(index(currentDefaultAccountIdx), index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + m_defaultAccount = newDefaultAccount; + onDefaultAccountChanged(); } } } @@ -165,6 +180,23 @@ void AccountList::accountChanged() onListChanged(); } +void AccountList::accountActivityChanged(bool active) +{ + MinecraftAccount *account = qobject_cast<MinecraftAccount *>(sender()); + bool found = false; + for (int i = 0; i < count(); i++) { + if (at(i).get() == account) { + emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1)); + found = true; + break; + } + } + if(found) { + emit listActivityChanged(); + } +} + + void AccountList::onListChanged() { if (m_autosave) @@ -174,12 +206,12 @@ void AccountList::onListChanged() emit listChanged(); } -void AccountList::onActiveChanged() +void AccountList::onDefaultAccountChanged() { if (m_autosave) saveList(); - emit activeAccountChanged(); + emit defaultAccountChanged(); } int AccountList::count() const @@ -211,6 +243,16 @@ QVariant AccountList::data(const QModelIndex &index, int role) const return typeStr; } + case StatusColumn: { + if(account->isActive()) { + return tr("Working", "Account status"); + } + if(account->isExpired()) { + return tr("Expired", "Account status"); + } + return tr("Ready", "Account status"); + } + case ProfileNameColumn: { return account->profileName(); } @@ -235,13 +277,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const return account->accountDisplayString(); case PointerRole: - return qVariantFromValue(account); + return QVariant::fromValue(account); case Qt::CheckStateRole: switch (index.column()) { case NameColumn: - return account == m_activeAccount ? Qt::Checked : Qt::Unchecked; + return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; } default: @@ -260,6 +302,8 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r return tr("Account"); case TypeColumn: return tr("Type"); + case StatusColumn: + return tr("Status"); case MigrationColumn: return tr("Can Migrate?"); case ProfileNameColumn: @@ -275,6 +319,8 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r return tr("User name of the account."); case TypeColumn: return tr("Type of the account - Mojang or MSA."); + case StatusColumn: + return tr("Current status of the account."); case MigrationColumn: return tr("Can this account migrate to Microsoft account?"); case ProfileNameColumn: @@ -309,9 +355,9 @@ Qt::ItemFlags AccountList::flags(const QModelIndex &index) const return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; } -bool AccountList::setData(const QModelIndex &index, const QVariant &value, int role) +bool AccountList::setData(const QModelIndex &idx, const QVariant &value, int role) { - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + if (idx.row() < 0 || idx.row() >= rowCount(idx) || !idx.isValid()) { return false; } @@ -320,12 +366,12 @@ bool AccountList::setData(const QModelIndex &index, const QVariant &value, int r { if(value == Qt::Checked) { - MinecraftAccountPtr account = at(index.row()); - setActiveAccount(account->profileId()); + MinecraftAccountPtr account = at(idx.row()); + setDefaultAccount(account); } } - emit dataChanged(index, index); + emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); return true; } @@ -395,7 +441,7 @@ bool AccountList::loadList() bool AccountList::loadV2(QJsonObject& root) { beginResetModel(); - auto activeUserName = root.value("activeAccount").toString(""); + auto defaultUserName = root.value("activeAccount").toString(""); QJsonArray accounts = root.value("accounts").toArray(); for (QJsonValue accountVal : accounts) { @@ -411,9 +457,10 @@ bool AccountList::loadV2(QJsonObject& root) { continue; } connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); m_accounts.append(account); - if (activeUserName.size() && account->mojangUserName() == activeUserName) { - m_activeAccount = account; + if (defaultUserName.size() && account->mojangUserName() == defaultUserName) { + m_defaultAccount = account; } } else @@ -435,16 +482,16 @@ bool AccountList::loadV3(QJsonObject& root) { if (account.get() != nullptr) { auto profileId = account->profileId(); - if(!profileId.size()) { - continue; - } - if(findAccountByProfileId(profileId) != -1) { - continue; + if(profileId.size()) { + 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(accountObj.value("active").toBool(false)) { - m_activeAccount = account; + m_defaultAccount = account; } } else @@ -491,7 +538,7 @@ bool AccountList::saveList() for (MinecraftAccountPtr account : m_accounts) { QJsonObject accountObj = account->saveToJson(); - if(m_activeAccount == account) { + if(m_defaultAccount == account) { accountObj["active"] = true; } accounts.append(accountObj); diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index e275eb17..08004628 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -42,11 +42,13 @@ public: ProfileNameColumn, MigrationColumn, TypeColumn, + StatusColumn, NUM_COLUMNS }; explicit AccountList(QObject *parent = 0); + virtual ~AccountList() noexcept; const MinecraftAccountPtr at(int i) const; int count() const; @@ -63,6 +65,7 @@ public: void removeAccount(QModelIndex index); int findAccountByProfileId(const QString &profileId) const; MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const; + QStringList profileNames() const; /*! * Sets the path to load/save the list file from/to. @@ -78,13 +81,14 @@ public: bool loadV3(QJsonObject &root); bool saveList(); - MinecraftAccountPtr activeAccount() const; - void setActiveAccount(const QString &profileId); + MinecraftAccountPtr defaultAccount() const; + void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); signals: void listChanged(); - void activeAccountChanged(); + void listActivityChanged(); + void defaultAccountChanged(); public slots: /** @@ -92,6 +96,11 @@ public slots: */ void accountChanged(); + /** + * This is called when a (refresh/login) task involving the account starts or ends + */ + void accountActivityChanged(bool active); + protected: /*! * Called whenever the list changes. @@ -101,13 +110,13 @@ protected: /*! * Called whenever the active account changes. - * Emits the activeAccountChanged() signal and autosaves the list if enabled. + * Emits the defaultAccountChanged() signal and autosaves the list if enabled. */ - void onActiveChanged(); + void onDefaultAccountChanged(); QList<MinecraftAccountPtr> m_accounts; - MinecraftAccountPtr m_activeAccount; + MinecraftAccountPtr m_defaultAccount; //! Path to the account list file. Empty string if there isn't one. QString m_listFilePath; diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index d400ce8d..25d753de 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -23,10 +23,6 @@ #include <QNetworkReply> #include <QByteArray> -#include <Env.h> - -#include <BuildConfig.h> - #include <QDebug> AccountTask::AccountTask(AccountData *data, QObject *parent) diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index f609d5d3..55fbdf39 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -3,8 +3,10 @@ #include <QString> #include <QMultiMap> #include <memory> +#include "QObjectPtr.h" class MinecraftAccount; +class QNetworkAccessManager; struct AuthSession { @@ -17,6 +19,7 @@ struct AuthSession Undetermined, RequiresOAuth, RequiresPassword, + RequiresProfileSetup, PlayableOffline, PlayableOnline, GoneOrMigrated @@ -40,7 +43,6 @@ struct AuthSession bool auth_server_online = false; // Did the user request online mode? bool wants_online = true; - std::shared_ptr<MinecraftAccount> m_accountPtr; }; typedef std::shared_ptr<AuthSession> AuthSessionPtr; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 2d76f9ac..30ed6afe 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -28,11 +28,16 @@ #include <QDebug> #include <QPainter> -#include <minecraft/auth/flows/MSASilent.h> -#include <minecraft/auth/flows/MSAInteractive.h> +#include "flows/MSASilent.h" +#include "flows/MSAInteractive.h" + +#include "flows/MojangRefresh.h" +#include "flows/MojangLogin.h" + +MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { + m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); +} -#include <minecraft/auth/flows/MojangRefresh.h> -#include <minecraft/auth/flows/MojangLogin.h> MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); @@ -52,7 +57,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) { - MinecraftAccountPtr account(new MinecraftAccount()); + MinecraftAccountPtr account = new MinecraftAccount(); account->data.type = AccountType::Mojang; account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); @@ -91,6 +96,23 @@ AccountStatus MinecraftAccount::accountStatus() const { } } +bool MinecraftAccount::isExpired() const { + switch(data.type) { + case AccountType::Mojang: { + return data.accessToken().isEmpty(); + } + break; + case AccountType::MSA: { + return data.msaToken.validity == Katabasis::Validity::None; + } + break; + default: { + return true; + } + } +} + + QPixmap MinecraftAccount::getFace() const { QPixmap skinTexture; if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { @@ -104,7 +126,7 @@ QPixmap MinecraftAccount::getFace() const { } -std::shared_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password) +shared_qobject_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password) { Q_ASSERT(m_currentTask.get() == nullptr); @@ -140,11 +162,12 @@ std::shared_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QSt connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); } return m_currentTask; } -std::shared_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) { +shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) { Q_ASSERT(m_currentTask.get() == nullptr); if(accountStatus() == Verified && !session->wants_online) @@ -161,11 +184,12 @@ std::shared_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); } return m_currentTask; } -std::shared_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) { +shared_qobject_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) { Q_ASSERT(m_currentTask.get() == nullptr); // take care of the true offline status @@ -203,6 +227,7 @@ std::shared_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) { connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); } return m_currentTask; } @@ -213,13 +238,27 @@ void MinecraftAccount::authSucceeded() auto session = m_currentTask->getAssignedSession(); if (session) { - session->status = - session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline; + /* + session->status = AuthSession::RequiresProfileSetup; + session->auth_server_online = true; + */ + if(data.profileId().size() == 0) { + session->status = AuthSession::RequiresProfileSetup; + } + else { + if(session->wants_online) { + session->status = AuthSession::PlayableOnline; + } + else { + session->status = AuthSession::PlayableOffline; + } + } fillSession(session); session->auth_server_online = true; } m_currentTask.reset(); emit changed(); + emit activityChanged(false); } void MinecraftAccount::authFailed(QString reason) @@ -284,6 +323,45 @@ void MinecraftAccount::authFailed(QString reason) } } m_currentTask.reset(); + emit activityChanged(false); +} + +bool MinecraftAccount::isActive() const { + return m_currentTask; +} + +bool MinecraftAccount::shouldRefresh() const { + /* + * Never refresh accounts that are being used by the game, it breaks the game session. + * Always refresh accounts that have not been refreshed yet during this session. + * Don't refresh broken accounts. + * Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours). + */ + if(isInUse()) { + return false; + } + switch(data.validity_) { + case Katabasis::Validity::Certain: { + break; + } + case Katabasis::Validity::None: { + return false; + } + case Katabasis::Validity::Assumed: { + return true; + } + } + auto now = QDateTime::currentDateTimeUtc(); + auto issuedTimestamp = data.yggdrasilToken.issueInstant; + auto expiresTimestamp = data.yggdrasilToken.notAfter; + + if(!expiresTimestamp.isValid()) { + expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); + } + if (now.secsTo(expiresTimestamp) < 12 * 3600) { + return true; + } + return false; } void MinecraftAccount::fillSession(AuthSessionPtr session) @@ -309,7 +387,6 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) { session->session = "-"; } - session->m_accountPtr = shared_from_this(); } void MinecraftAccount::decrementUses() diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 5b0c1ec7..459ef903 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -27,12 +27,13 @@ #include "AuthSession.h" #include "Usable.h" #include "AccountData.h" +#include "QObjectPtr.h" class Task; class AccountTask; class MinecraftAccount; -typedef std::shared_ptr<MinecraftAccount> MinecraftAccountPtr; +typedef shared_qobject_ptr<MinecraftAccount> MinecraftAccountPtr; Q_DECLARE_METATYPE(MinecraftAccountPtr) /** @@ -63,8 +64,7 @@ enum AccountStatus */ class MinecraftAccount : public QObject, - public Usable, - public std::enable_shared_from_this<MinecraftAccount> + public Usable { Q_OBJECT public: /* construction */ @@ -72,7 +72,7 @@ public: /* construction */ explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete; //! Default constructor - explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {}; + explicit MinecraftAccount(QObject *parent = 0); static MinecraftAccountPtr createFromUsername(const QString &username); @@ -90,13 +90,17 @@ public: /* manipulation */ * Attempt to login. Empty password means we use the token. * If the attempt fails because we already are performing some task, it returns false. */ - std::shared_ptr<AccountTask> login(AuthSessionPtr session, QString password = QString()); + shared_qobject_ptr<AccountTask> login(AuthSessionPtr session, QString password); - std::shared_ptr<AccountTask> loginMSA(AuthSessionPtr session); + shared_qobject_ptr<AccountTask> loginMSA(AuthSessionPtr session); - std::shared_ptr<AccountTask> refresh(AuthSessionPtr session); + shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session); public: /* queries */ + QString internalId() const { + return m_internalId; + } + QString accountDisplayString() const { return data.accountDisplayString(); } @@ -117,6 +121,10 @@ public: /* queries */ return data.profileName(); } + bool isActive() const; + + bool isExpired() const; + bool canMigrate() const { return data.canMigrateToMSA; } @@ -153,19 +161,24 @@ public: /* queries */ return &data; } + bool shouldRefresh() const; + signals: /** * This signal is emitted when the account changes */ void changed(); + void activityChanged(bool active); + // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ + QString m_internalId; AccountData data; // current task we are executing here - std::shared_ptr<AccountTask> m_currentTask; + shared_qobject_ptr<AccountTask> m_currentTask; protected: /* methods */ diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9fb3ec48..00957fd4 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -4,25 +4,21 @@ #include <QDesktopServices> #include <QMetaEnum> #include <QDebug> - #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> - +#include <QUuid> #include <QUrlQuery> -#include <QPixmap> -#include <QPainter> - #include "AuthContext.h" #include "katabasis/Globals.h" #include "AuthRequest.h" -#include "Secrets.h" +#include "Parsers.h" -#include "Env.h" +#include <Application.h> -using OAuth2 = Katabasis::OAuth2; +using OAuth2 = Katabasis::DeviceFlow; using Activity = Katabasis::Activity; AuthContext::AuthContext(AccountData * data, QObject *parent) : @@ -54,25 +50,17 @@ void AuthContext::initMSA() { return; } - auto clientId = Secrets::getMSAClientID('-'); - if(clientId.isEmpty()) { - return; - } - - Katabasis::OAuth2::Options opts; + OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = clientId; + opts.clientIdentifier = APPLICATION->msaClientId(); opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; - opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; - m_oauth2 = new OAuth2(opts, m_data->msaToken, this, &ENV.qnam()); - m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice); + // FIXME: OAuth2 is not aware of our fancy shared pointers + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); - connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed); - connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); - connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); } void AuthContext::initMojang() { @@ -97,50 +85,56 @@ void AuthContext::onMojangFailed() { changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); } -/* -bool AuthContext::signOut() { - if(isBusy()) { - return false; - } - - start(); - - beginActivity(Activity::LoggingOut); - m_oauth2->unlink(); - m_account = AccountData(); - finishActivity(); - return true; -} -*/ - -void AuthContext::onOAuthLinkingFailed() { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); -} - -void AuthContext::onOAuthLinkingSucceeded() { - emit hideVerificationUriAndCode(); - auto *o2t = qobject_cast<OAuth2 *>(sender()); - if (!o2t->linked()) { - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); - return; - } - QVariantMap extraTokens = o2t->extraTokens(); -#ifndef NDEBUG - if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; - foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); +void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::Refreshing: + case Katabasis::Activity::LoggingOut: { + // We asked it to do something, it's doing it. Nothing to act upon. + return; } - } + case Katabasis::Activity::Succeeded: { + // Succeeded or did not invalidate tokens + emit hideVerificationUriAndCode(); + if (!m_oauth2->linked()) { + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); + return; + } + QVariantMap extraTokens = m_oauth2->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } #endif - doUserAuth(); -} + doUserAuth(); + return; + } + case Katabasis::Activity::FailedSoft: { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error.")); + return; + } + case Katabasis::Activity::FailedGone: + case Katabasis::Activity::FailedHard: { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); + return; + } + default: { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); + return; + } -void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { - // respond to activity change here + } } void AuthContext::doUserAuth() { @@ -169,137 +163,6 @@ void AuthContext::doUserAuth() { qDebug() << "First layer of XBox auth ... commencing."; } -namespace { -bool getDateTime(QJsonValue value, QDateTime & out) { - if(!value.isString()) { - return false; - } - out = QDateTime::fromString(value.toString(), Qt::ISODate); - return out.isValid(); -} - -bool getString(QJsonValue value, QString & out) { - if(!value.isString()) { - return false; - } - out = value.toString(); - return true; -} - -bool getNumber(QJsonValue value, double & out) { - if(!value.isDouble()) { - return false; - } - out = value.toDouble(); - return true; -} - -bool getNumber(QJsonValue value, int64_t & out) { - if(!value.isDouble()) { - return false; - } - out = (int64_t) value.toDouble(); - return true; -} - -bool getBool(QJsonValue value, bool & out) { - if(!value.isBool()) { - return false; - } - out = value.toBool(); - return true; -} - -/* -{ - "IssueInstant":"2020-12-07T19:52:08.4463796Z", - "NotAfter":"2020-12-21T19:52:08.4463796Z", - "Token":"token", - "DisplayClaims":{ - "xui":[ - { - "uhs":"userhash" - } - ] - } - } -*/ -// TODO: handle error responses ... -/* -{ - "Identity":"0", - "XErr":2148916238, - "Message":"", - "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" -} -// 2148916233 = missing XBox account -// 2148916238 = child account not linked to a family -*/ - -bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { - qDebug() << "Parsing" << name <<":"; -#ifndef NDEBUG - qDebug() << data; -#endif - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { - qWarning() << "User IssueInstant is not a timestamp"; - return false; - } - if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { - qWarning() << "User NotAfter is not a timestamp"; - return false; - } - if(!getString(obj.value("Token"), output.token)) { - qWarning() << "User Token is not a timestamp"; - return false; - } - auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); - if(!arrayVal.isArray()) { - qWarning() << "Missing xui claims array"; - return false; - } - bool foundUHS = false; - for(auto item: arrayVal.toArray()) { - if(!item.isObject()) { - continue; - } - auto obj = item.toObject(); - if(obj.contains("uhs")) { - foundUHS = true; - } else { - continue; - } - // consume all 'display claims' ... whatever that means - for(auto iter = obj.begin(); iter != obj.end(); iter++) { - QString claim; - if(!getString(obj.value(iter.key()), claim)) { - qWarning() << "display claim " << iter.key() << " is not a string..."; - return false; - } - output.extra[iter.key()] = claim; - } - - break; - } - if(!foundUHS) { - qWarning() << "Missing uhs"; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << name << "is valid."; - return true; -} - -} - void AuthContext::onUserAuthDone( QNetworkReply::NetworkError error, QByteArray replyData, @@ -313,7 +176,7 @@ void AuthContext::onUserAuthDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "UToken")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; finishActivity(); changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); @@ -365,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() { void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) { if(error == QNetworkReply::AuthenticationRequiredError) { - QJsonParseError jsonError; + QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); @@ -374,7 +237,7 @@ void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray int64_t errorCode = -1; auto obj = doc.object(); - if(!getNumber(obj.value("XErr"), errorCode)) { + if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { qWarning() << "XErr is not a number"; return; } @@ -400,7 +263,7 @@ void AuthContext::onSTSAuthMinecraftDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { qWarning() << "Could not parse authorization response for access to mojang services..."; failResult(m_mcAuthSucceeded); return; @@ -417,67 +280,33 @@ void AuthContext::onSTSAuthMinecraftDone( } void AuthContext::doMinecraftAuth() { + auto requestURL = "https://api.minecraftservices.com/launcher/login"; + auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); + auto xToken = m_data->mojangservicesToken.token; + QString mc_auth_template = R"XXX( { - "identityToken": "XBL3.0 x=%1;%2" + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" } )XXX"; - auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->mojangservicesToken.token); + auto requestBody = mc_auth_template.arg(uhs, xToken); - QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox")); + QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); AuthRequest *requestor = new AuthRequest(this); connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftAuthDone); - requestor->post(request, data.toUtf8()); + requestor->post(request, requestBody.toUtf8()); qDebug() << "Getting Minecraft access token..."; } -namespace { -bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { - QJsonParseError jsonError; - qDebug() << "Parsing Mojang response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - double expires_in = 0; - if(!getNumber(obj.value("expires_in"), expires_in)) { - qWarning() << "expires_in is not a valid number"; - return false; - } - auto currentTime = QDateTime::currentDateTimeUtc(); - output.issueInstant = currentTime; - output.notAfter = currentTime.addSecs(expires_in); - - QString username; - if(!getString(obj.value("username"), username)) { - qWarning() << "username is not valid"; - return false; - } - - // TODO: it's a JWT... validate it? - if(!getString(obj.value("access_token"), output.token)) { - qWarning() << "access_token is not valid"; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << "Mojang response is valid."; - return true; -} -} - void AuthContext::onMinecraftAuthDone( QNetworkReply::NetworkError error, QByteArray replyData, QList<QNetworkReply::RawHeaderPair> headers ) { + qDebug() << replyData; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG @@ -487,7 +316,7 @@ void AuthContext::onMinecraftAuthDone( return; } - if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { + if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; #ifndef NDEBUG qDebug() << replyData; @@ -539,7 +368,7 @@ void AuthContext::onSTSAuthGenericDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { qWarning() << "Could not parse authorization response for access to xbox API..."; failResult(m_xboxProfileSucceeded); return; @@ -619,7 +448,7 @@ void AuthContext::checkResult() { return; } if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { - doMinecraftProfile(); + doEntitlements(); } else { finishActivity(); @@ -662,84 +491,33 @@ void AuthContext::checkResult() { } } -namespace { -bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { - qDebug() << "Parsing Minecraft profile..."; +void AuthContext::doEntitlements() { + auto uuid = QUuid::createUuid(); + entitlementsRequestId = uuid.toString().remove('{').remove('}'); + auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + entitlementsRequestId; + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &AuthContext::onEntitlementsDone); + requestor->get(request); + qDebug() << "Getting Xbox profile..."; +} + + +void AuthContext::onEntitlementsDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { #ifndef NDEBUG qDebug() << data; #endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - if(!getString(obj.value("id"), output.id)) { - qWarning() << "Minecraft profile id is not a string"; - return false; - } - - if(!getString(obj.value("name"), output.name)) { - qWarning() << "Minecraft profile name is not a string"; - return false; - } - - auto skinsArray = obj.value("skins").toArray(); - for(auto skin: skinsArray) { - auto skinObj = skin.toObject(); - Skin skinOut; - if(!getString(skinObj.value("id"), skinOut.id)) { - continue; - } - QString state; - if(!getString(skinObj.value("state"), state)) { - continue; - } - if(state != "ACTIVE") { - continue; - } - if(!getString(skinObj.value("url"), skinOut.url)) { - continue; - } - if(!getString(skinObj.value("variant"), skinOut.variant)) { - continue; - } - // we deal with only the active skin - output.skin = skinOut; - break; - } - auto capesArray = obj.value("capes").toArray(); - - QString currentCape; - for(auto cape: capesArray) { - auto capeObj = cape.toObject(); - Cape capeOut; - if(!getString(capeObj.value("id"), capeOut.id)) { - continue; - } - QString state; - if(!getString(capeObj.value("state"), state)) { - continue; - } - if(state == "ACTIVE") { - currentCape = capeOut.id; - } - if(!getString(capeObj.value("url"), capeOut.url)) { - continue; - } - if(!getString(capeObj.value("alias"), capeOut.alias)) { - continue; - } - - output.capes[capeOut.id] = capeOut; - } - output.currentCape = currentCape; - output.validity = Katabasis::Validity::Certain; - return true; -} + // TODO: check presence of same entitlementsRequestId? + // TODO: validate JWTs? + Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + doMinecraftProfile(); } void AuthContext::doMinecraftProfile() { @@ -766,9 +544,13 @@ void AuthContext::onMinecraftProfileDone( qDebug() << data; #endif if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } m_data->minecraftProfile = MinecraftProfile(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); + succeed(); return; } if (error != QNetworkReply::NoError) { @@ -776,7 +558,7 @@ void AuthContext::onMinecraftProfileDone( changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); return; } - if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { + if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); @@ -784,6 +566,9 @@ void AuthContext::onMinecraftProfileDone( } if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; doMigrationEligibilityCheck(); } else { @@ -805,43 +590,13 @@ void AuthContext::doMigrationEligibilityCheck() { requestor->get(request); } -bool parseRolloutResponse(QByteArray & data, bool& result) { - qDebug() << "Parsing Rollout response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - QString feature; - if(!getString(obj.value("feature"), feature)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - if(feature != "msamigration") { - qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; - return false; - } - if(!getBool(obj.value("rollout"), result)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - return true; -} - void AuthContext::onMigrationEligibilityCheckDone( QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers ) { if (error == QNetworkReply::NoError) { - parseRolloutResponse(data, m_data->canMigrateToMSA); + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); } doGetSkin(); } @@ -865,6 +620,11 @@ void AuthContext::onSkinDone( if (error == QNetworkReply::NoError) { m_data->minecraftProfile.skin.data = data; } + succeed(); + +} + +void AuthContext::succeed() { m_data->validity_ = Katabasis::Validity::Certain; finishActivity(); changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index dc7552ac..5e4e9edc 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -7,7 +7,7 @@ #include <QNetworkReply> #include <QImage> -#include <katabasis/OAuth2.h> +#include <katabasis/DeviceFlow.h> #include "Yggdrasil.h" #include "../AccountData.h" #include "../AccountTask.h" @@ -35,9 +35,6 @@ signals: private slots: // OAuth-specific callbacks - void onOAuthLinkingSucceeded(); - void onOAuthLinkingFailed(); - void onOAuthActivityChanged(Katabasis::Activity activity); // Yggdrasil specific callbacks @@ -63,6 +60,9 @@ protected: void doXBoxProfile(); Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void doEntitlements(); + Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void doMinecraftProfile(); Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); @@ -72,6 +72,8 @@ protected: void doGetSkin(); Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void succeed(); + void failResult(bool & flag); void succeedResult(bool & flag); void checkResult(); @@ -82,12 +84,13 @@ protected: void clearTokens(); protected: - Katabasis::OAuth2 *m_oauth2 = nullptr; + Katabasis::DeviceFlow *m_oauth2 = nullptr; Yggdrasil *m_yggdrasil = nullptr; int m_requestsDone = 0; bool m_xboxProfileSucceeded = false; bool m_mcAuthSucceeded = false; + QString entitlementsRequestId; QSet<int64_t> stsErrors; bool stsFailed = false; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/flows/AuthRequest.cpp index 77558fd3..82dba591 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.cpp +++ b/launcher/minecraft/auth/flows/AuthRequest.cpp @@ -5,9 +5,9 @@ #include <QBuffer> #include <QUrlQuery> +#include "Application.h" #include "AuthRequest.h" #include "katabasis/Globals.h" -#include "Env.h" AuthRequest::AuthRequest(QObject *parent): QObject(parent) { } @@ -17,7 +17,7 @@ AuthRequest::~AuthRequest() { void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { setup(req, QNetworkAccessManager::GetOperation); - reply_ = ENV.qnam().get(request_); + reply_ = APPLICATION->network()->get(request_); status_ = Requesting; timedReplies_.add(new Katabasis::Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); @@ -29,7 +29,7 @@ void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int t setup(req, QNetworkAccessManager::PostOperation); data_ = data; status_ = Requesting; - reply_ = ENV.qnam().post(request_, data_); + reply_ = APPLICATION->network()->post(request_, data_); timedReplies_.add(new Katabasis::Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/flows/AuthRequest.h index 6a45a0bd..a547aea4 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.h +++ b/launcher/minecraft/auth/flows/AuthRequest.h @@ -5,7 +5,6 @@ #include <QNetworkAccessManager> #include <QUrl> #include <QByteArray> -#include <QHttpMultiPart> #include "katabasis/Reply.h" diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp index 03beb279..525aaf88 100644 --- a/launcher/minecraft/auth/flows/MSAInteractive.cpp +++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp @@ -1,6 +1,9 @@ #include "MSAInteractive.h" -MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthContext(data, parent) {} +MSAInteractive::MSAInteractive( + AccountData* data, + QObject* parent +) : AuthContext(data, parent) {} void MSAInteractive::executeTask() { m_requestsDone = 0; @@ -14,7 +17,6 @@ void MSAInteractive::executeTask() { m_oauth2->setExtraRequestParams(extraOpts); beginActivity(Katabasis::Activity::LoggingIn); - m_oauth2->unlink(); *m_data = AccountData(); - m_oauth2->link(); + m_oauth2->login(); } diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h index 9556f254..6654e0d6 100644 --- a/launcher/minecraft/auth/flows/MSAInteractive.h +++ b/launcher/minecraft/auth/flows/MSAInteractive.h @@ -5,6 +5,9 @@ class MSAInteractive : public AuthContext { Q_OBJECT public: - explicit MSAInteractive(AccountData * data, QObject *parent = 0); + explicit MSAInteractive( + AccountData *data, + QObject *parent = 0 + ); void executeTask() override; }; diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h index e1b3d43d..a442b49e 100644 --- a/launcher/minecraft/auth/flows/MSASilent.h +++ b/launcher/minecraft/auth/flows/MSASilent.h @@ -5,6 +5,9 @@ class MSASilent : public AuthContext { Q_OBJECT public: - explicit MSASilent(AccountData * data, QObject *parent = 0); + explicit MSASilent( + AccountData * data, + QObject *parent = 0 + ); void executeTask() override; }; diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp index cca911b5..6c217cd1 100644 --- a/launcher/minecraft/auth/flows/MojangLogin.cpp +++ b/launcher/minecraft/auth/flows/MojangLogin.cpp @@ -1,6 +1,10 @@ #include "MojangLogin.h" -MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthContext(data, parent), m_password(password) {} +MojangLogin::MojangLogin( + AccountData *data, + QString password, + QObject *parent +): AuthContext(data, parent), m_password(password) {} void MojangLogin::executeTask() { m_requestsDone = 0; diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h index 2e765ae8..5f33752f 100644 --- a/launcher/minecraft/auth/flows/MojangLogin.h +++ b/launcher/minecraft/auth/flows/MojangLogin.h @@ -5,7 +5,11 @@ class MojangLogin : public AuthContext { Q_OBJECT public: - explicit MojangLogin(AccountData * data, QString password, QObject *parent = 0); + explicit MojangLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); void executeTask() override; private: diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp index af99175c..008c0453 100644 --- a/launcher/minecraft/auth/flows/MojangRefresh.cpp +++ b/launcher/minecraft/auth/flows/MojangRefresh.cpp @@ -1,6 +1,9 @@ #include "MojangRefresh.h" -MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthContext(data, parent) {} +MojangRefresh::MojangRefresh( + AccountData *data, + QObject *parent +) : AuthContext(data, parent) {} void MojangRefresh::executeTask() { m_requestsDone = 0; diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h index fb4facd5..06e4e4ce 100644 --- a/launcher/minecraft/auth/flows/MojangRefresh.h +++ b/launcher/minecraft/auth/flows/MojangRefresh.h @@ -5,6 +5,6 @@ class MojangRefresh : public AuthContext { Q_OBJECT public: - explicit MojangRefresh(AccountData * data, QObject *parent = 0); + explicit MojangRefresh(AccountData *data, QObject *parent = 0); void executeTask() override; }; diff --git a/launcher/minecraft/auth/flows/Parsers.cpp b/launcher/minecraft/auth/flows/Parsers.cpp new file mode 100644 index 00000000..ecb11cf9 --- /dev/null +++ b/launcher/minecraft/auth/flows/Parsers.cpp @@ -0,0 +1,316 @@ +#include "Parsers.h" + +#include <QJsonDocument> +#include <QJsonArray> +#include <QDebug> + +namespace Parsers { + +bool getDateTime(QJsonValue value, QDateTime & out) { + if(!value.isString()) { + return false; + } + out = QDateTime::fromString(value.toString(), Qt::ISODate); + return out.isValid(); +} + +bool getString(QJsonValue value, QString & out) { + if(!value.isString()) { + return false; + } + out = value.toString(); + return true; +} + +bool getNumber(QJsonValue value, double & out) { + if(!value.isDouble()) { + return false; + } + out = value.toDouble(); + return true; +} + +bool getNumber(QJsonValue value, int64_t & out) { + if(!value.isDouble()) { + return false; + } + out = (int64_t) value.toDouble(); + return true; +} + +bool getBool(QJsonValue value, bool & out) { + if(!value.isBool()) { + return false; + } + out = value.toBool(); + return true; +} + +/* +{ + "IssueInstant":"2020-12-07T19:52:08.4463796Z", + "NotAfter":"2020-12-21T19:52:08.4463796Z", + "Token":"token", + "DisplayClaims":{ + "xui":[ + { + "uhs":"userhash" + } + ] + } + } +*/ +// TODO: handle error responses ... +/* +{ + "Identity":"0", + "XErr":2148916238, + "Message":"", + "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" +} +// 2148916233 = missing XBox account +// 2148916238 = child account not linked to a family +*/ + +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { + qDebug() << "Parsing" << name <<":"; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { + qWarning() << "User IssueInstant is not a timestamp"; + return false; + } + if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { + qWarning() << "User NotAfter is not a timestamp"; + return false; + } + if(!getString(obj.value("Token"), output.token)) { + qWarning() << "User Token is not a timestamp"; + return false; + } + auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); + if(!arrayVal.isArray()) { + qWarning() << "Missing xui claims array"; + return false; + } + bool foundUHS = false; + for(auto item: arrayVal.toArray()) { + if(!item.isObject()) { + continue; + } + auto obj = item.toObject(); + if(obj.contains("uhs")) { + foundUHS = true; + } else { + continue; + } + // consume all 'display claims' ... whatever that means + for(auto iter = obj.begin(); iter != obj.end(); iter++) { + QString claim; + if(!getString(obj.value(iter.key()), claim)) { + qWarning() << "display claim " << iter.key() << " is not a string..."; + return false; + } + output.extra[iter.key()] = claim; + } + + break; + } + if(!foundUHS) { + qWarning() << "Missing uhs"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << name << "is valid."; + return true; +} + +bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto skinsArray = obj.value("skins").toArray(); + for(auto skin: skinsArray) { + auto skinObj = skin.toObject(); + Skin skinOut; + if(!getString(skinObj.value("id"), skinOut.id)) { + continue; + } + QString state; + if(!getString(skinObj.value("state"), state)) { + continue; + } + if(state != "ACTIVE") { + continue; + } + if(!getString(skinObj.value("url"), skinOut.url)) { + continue; + } + if(!getString(skinObj.value("variant"), skinOut.variant)) { + continue; + } + // we deal with only the active skin + output.skin = skinOut; + break; + } + auto capesArray = obj.value("capes").toArray(); + + QString currentCape; + for(auto cape: capesArray) { + auto capeObj = cape.toObject(); + Cape capeOut; + if(!getString(capeObj.value("id"), capeOut.id)) { + continue; + } + QString state; + if(!getString(capeObj.value("state"), state)) { + continue; + } + if(state == "ACTIVE") { + currentCape = capeOut.id; + } + if(!getString(capeObj.value("url"), capeOut.url)) { + continue; + } + if(!getString(capeObj.value("alias"), capeOut.alias)) { + continue; + } + + output.capes[capeOut.id] = capeOut; + } + output.currentCape = currentCape; + output.validity = Katabasis::Validity::Certain; + return true; +} + +bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { + qDebug() << "Parsing Minecraft entitlements..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + + auto itemsArray = obj.value("items").toArray(); + for(auto item: itemsArray) { + auto itemObj = item.toObject(); + QString name; + if(!getString(itemObj.value("name"), name)) { + continue; + } + if(name == "game_minecraft") { + output.canPlayMinecraft = true; + } + if(name == "product_minecraft") { + output.ownsMinecraft = true; + } + } + output.validity = Katabasis::Validity::Certain; + return true; +} + +bool parseRolloutResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigration") { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + +bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { + QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + double expires_in = 0; + if(!getNumber(obj.value("expires_in"), expires_in)) { + qWarning() << "expires_in is not a valid number"; + return false; + } + auto currentTime = QDateTime::currentDateTimeUtc(); + output.issueInstant = currentTime; + output.notAfter = currentTime.addSecs(expires_in); + + QString username; + if(!getString(obj.value("username"), username)) { + qWarning() << "username is not valid"; + return false; + } + + // TODO: it's a JWT... validate it? + if(!getString(obj.value("access_token"), output.token)) { + qWarning() << "access_token is not valid"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << "Mojang response is valid."; + return true; +} + +} diff --git a/launcher/minecraft/auth/flows/Parsers.h b/launcher/minecraft/auth/flows/Parsers.h new file mode 100644 index 00000000..b484a073 --- /dev/null +++ b/launcher/minecraft/auth/flows/Parsers.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../AccountData.h" + +namespace Parsers +{ + bool getDateTime(QJsonValue value, QDateTime & out); + bool getString(QJsonValue value, QString & out); + bool getNumber(QJsonValue value, double & out); + bool getNumber(QJsonValue value, int64_t & out); + bool getBool(QJsonValue value, bool & out); + + bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name); + bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); + + bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); + bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); + bool parseRolloutResponse(QByteArray &data, bool& result); +} diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp index 20ca63d0..5ea168e8 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.cpp +++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp @@ -23,12 +23,10 @@ #include <QNetworkReply> #include <QByteArray> -#include <Env.h> - -#include <BuildConfig.h> - #include <QDebug> +#include "Application.h" + Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) : AccountTask(data, parent) { @@ -40,7 +38,7 @@ void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { QNetworkRequest netRequest(endpoint); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_netReply = ENV.qnam().post(netRequest, content); + 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); @@ -86,7 +84,7 @@ void Yggdrasil::refresh() { req.insert("requestUser", false); QJsonDocument doc(req); - QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh"); + QUrl reqUrl("https://authserver.mojang.com/refresh"); QByteArray requestData = doc.toJson(); sendRequest(reqUrl, requestData); @@ -131,7 +129,7 @@ void Yggdrasil::login(QString password) { QJsonDocument doc(req); - QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate"); + QUrl reqUrl("https://authserver.mojang.com/authenticate"); QNetworkRequest netRequest(reqUrl); QByteArray requestData = doc.toJson(); @@ -140,20 +138,18 @@ void Yggdrasil::login(QString password) { -void Yggdrasil::refreshTimers(qint64, qint64) -{ +void Yggdrasil::refreshTimers(qint64, qint64) { timeout_keeper.stop(); timeout_keeper.start(timeout_max); progress(count = 0, timeout_max); } -void Yggdrasil::heartbeat() -{ + +void Yggdrasil::heartbeat() { count += time_step; progress(count, timeout_max); } -bool Yggdrasil::abort() -{ +bool Yggdrasil::abort() { progress(timeout_max, timeout_max); // TODO: actually use this in a meaningful way m_aborted = Yggdrasil::BY_USER; @@ -161,19 +157,16 @@ bool Yggdrasil::abort() return true; } -void Yggdrasil::abortByTimeout() -{ +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<QSslError> errors) -{ +void Yggdrasil::sslErrors(QList<QSslError> errors) { int i = 1; - for (auto error : errors) - { + for (auto error : errors) { qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); auto cert = error.certificate(); qCritical() << "Certificate in question:\n" << cert.toText(); @@ -181,8 +174,7 @@ void Yggdrasil::sslErrors(QList<QSslError> errors) } } -void Yggdrasil::processResponse(QJsonObject responseData) -{ +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."; @@ -191,8 +183,7 @@ void Yggdrasil::processResponse(QJsonObject 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()) - { + if (clientToken.isEmpty()) { // Fail if the server gave us an empty client token changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); return; @@ -208,8 +199,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) // Now, we set the access token. qDebug() << "Getting access token."; QString accessToken = responseData.value("accessToken").toString(""); - if (accessToken.isEmpty()) - { + if (accessToken.isEmpty()) { // Fail if the server didn't give us an access token. changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); return; @@ -217,6 +207,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) // Set the access token. m_data->yggdrasilToken.token = accessToken; m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; + m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. @@ -224,8 +215,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) changeState(STATE_SUCCEEDED); } -void Yggdrasil::processReply() -{ +void Yggdrasil::processReply() { changeState(STATE_WORKING); switch (m_netReply->error()) @@ -247,9 +237,9 @@ void Yggdrasil::processReply() "<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>" "<li>Some device on your network is interfering with SSL traffic. In that case, " "you have bigger worries than Minecraft not starting.</li>" - "<li>Possibly something else. Check the %1 log file for details</li>" + "<li>Possibly something else. Check the log file for details</li>" "</ul>" - ).arg(BuildConfig.LAUNCHER_NAME) + ) ); return; // used for invalid credentials and similar errors. Fall through. @@ -278,19 +268,16 @@ void Yggdrasil::processReply() // Check the response code. int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (responseCode == 200) - { + 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) - { + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); return; } - else - { + else { changeState( STATE_FAILED_SOFT, tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) @@ -304,16 +291,14 @@ void Yggdrasil::processReply() // 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) - { + 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 - { + 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."; @@ -324,14 +309,12 @@ void Yggdrasil::processReply() } } -void Yggdrasil::processError(QJsonObject responseData) -{ +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()) - { + if (errorVal.isString() && errorMessageValue.isString()) { m_error = std::shared_ptr<Error>( new Error { errorVal.toString(""), @@ -341,8 +324,7 @@ void Yggdrasil::processError(QJsonObject responseData) ); changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); } - else - { + else { // Error is not in standard format. Don't set m_error and return unknown error. changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); } diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/flows/Yggdrasil.h index e709cb9f..b9670ec7 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.h +++ b/launcher/minecraft/auth/flows/Yggdrasil.h @@ -24,6 +24,7 @@ #include "../MinecraftAccount.h" +class QNetworkAccessManager; class QNetworkReply; /** @@ -33,7 +34,10 @@ class Yggdrasil : public AccountTask { Q_OBJECT public: - explicit Yggdrasil(AccountData * data, QObject *parent = 0); + explicit Yggdrasil( + AccountData *data, + QObject *parent = 0 + ); virtual ~Yggdrasil() {}; void refresh(); diff --git a/launcher/minecraft/launch/ClaimAccount.cpp b/launcher/minecraft/launch/ClaimAccount.cpp index a1180f0a..bb4f6806 100644 --- a/launcher/minecraft/launch/ClaimAccount.cpp +++ b/launcher/minecraft/launch/ClaimAccount.cpp @@ -1,11 +1,15 @@ #include "ClaimAccount.h" #include <launch/LaunchTask.h> +#include "Application.h" +#include "minecraft/auth/AccountList.h" + ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session): LaunchStep(parent) { if(session->status == AuthSession::Status::PlayableOnline) { - m_account = session->m_accountPtr; + auto accounts = APPLICATION->accounts(); + m_account = accounts->getAccountByProfileName(session->player_name); } } diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index ff022c40..8fd11eca 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -14,13 +14,14 @@ */ #include "LauncherPartLaunch.h" -#include <QCoreApplication> -#include <launch/LaunchTask.h> -#include <minecraft/MinecraftInstance.h> -#include <FileSystem.h> -#include <Commandline.h> + #include <QStandardPaths> -#include "Env.h" + +#include "launch/LaunchTask.h" +#include "minecraft/MinecraftInstance.h" +#include "FileSystem.h" +#include "Commandline.h" +#include "Application.h" LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) { @@ -72,7 +73,7 @@ void LauncherPartLaunch::executeTask() m_process.setDetachable(true); auto classPath = minecraftInstance->getClassPath(); - classPath.prepend(FS::PathCombine(ENV.getJarsPath(), "NewLaunch.jar")); + classPath.prepend(FS::PathCombine(APPLICATION->getJarsPath(), "NewLaunch.jar")); auto natPath = minecraftInstance->getNativePath(); #ifdef Q_OS_WIN diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index 657669af..d9f7ecdc 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -11,8 +11,17 @@ void VerifyJavaInstall::executeTask() { auto javaVersion = m_inst->getJavaVersion(); auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft"); + // Java 17 requirement + if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java17BeginsDate) { + if (javaVersion.major() < 17) { + emit logLine("Minecraft 1.18 Pre Release 2 and above require the use of Java 17", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 1.18 Pre Release 2 and above require the use of Java 17")); + return; + } + } // Java 16 requirement - if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { + else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { if (javaVersion.major() < 16) { emit logLine("Minecraft 21w19a and above require the use of Java 16", MessageLevel::Fatal); diff --git a/launcher/minecraft/legacy/LegacyInstance.cpp b/launcher/minecraft/legacy/LegacyInstance.cpp index 9f9bda5a..c2b4309c 100644 --- a/launcher/minecraft/legacy/LegacyInstance.cpp +++ b/launcher/minecraft/legacy/LegacyInstance.cpp @@ -90,7 +90,7 @@ bool LegacyInstance::shouldUseCustomBaseJar() const } -shared_qobject_ptr<Task> LegacyInstance::createUpdateTask(Net::Mode) +Task::Ptr LegacyInstance::createUpdateTask(Net::Mode) { return nullptr; } diff --git a/launcher/minecraft/legacy/LegacyInstance.h b/launcher/minecraft/legacy/LegacyInstance.h index ac2a8543..c6680fd0 100644 --- a/launcher/minecraft/legacy/LegacyInstance.h +++ b/launcher/minecraft/legacy/LegacyInstance.h @@ -93,7 +93,7 @@ public: }; virtual bool shouldUpdate() const; - virtual shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override; + virtual Task::Ptr createUpdateTask(Net::Mode mode) override; virtual QString typeName() const override; diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index c1d88d14..d411965a 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -1,7 +1,9 @@ #include "CapeChange.h" + #include <QNetworkRequest> #include <QHttpMultiPart> -#include <Env.h> + +#include "Application.h" CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) : Task(parent), m_capeId(cape), m_session(session) @@ -12,11 +14,11 @@ void CapeChange::setCape(QString& cape) { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); - QNetworkReply *rep = ENV.qnam().put(request, requestString.toUtf8()); + QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8()); setStatus(tr("Equipping cape")); - m_reply = std::shared_ptr<QNetworkReply>(rep); + m_reply = shared_qobject_ptr<QNetworkReply>(rep); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); @@ -26,11 +28,11 @@ void CapeChange::clearCape() { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); - QNetworkReply *rep = ENV.qnam().deleteResource(request); + QNetworkReply *rep = APPLICATION->network()->deleteResource(request); setStatus(tr("Removing cape")); - m_reply = std::shared_ptr<QNetworkReply>(rep); + m_reply = shared_qobject_ptr<QNetworkReply>(rep); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h index 1b6f2f72..c04ad8c7 100644 --- a/launcher/minecraft/services/CapeChange.h +++ b/launcher/minecraft/services/CapeChange.h @@ -5,6 +5,7 @@ #include <memory> #include <minecraft/auth/AuthSession.h> #include "tasks/Task.h" +#include "QObjectPtr.h" class CapeChange : public Task { @@ -20,7 +21,7 @@ private: private: QString m_capeId; AuthSessionPtr m_session; - std::shared_ptr<QNetworkReply> m_reply; + shared_qobject_ptr<QNetworkReply> m_reply; protected: virtual void executeTask(); diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp index 34977257..a0b0330c 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/services/SkinDelete.cpp @@ -1,7 +1,9 @@ #include "SkinDelete.h" + #include <QNetworkRequest> #include <QHttpMultiPart> -#include <Env.h> + +#include "Application.h" SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session) : Task(parent), m_session(session) @@ -12,8 +14,8 @@ void SkinDelete::executeTask() { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); - QNetworkReply *rep = ENV.qnam().deleteResource(request); - m_reply = std::shared_ptr<QNetworkReply>(rep); + QNetworkReply *rep = APPLICATION->network()->deleteResource(request); + m_reply = shared_qobject_ptr<QNetworkReply>(rep); setStatus(tr("Deleting skin")); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h index 839bf9bc..6048b33a 100644 --- a/launcher/minecraft/services/SkinDelete.h +++ b/launcher/minecraft/services/SkinDelete.h @@ -2,11 +2,10 @@ #include <QFile> #include <QtNetwork/QtNetwork> -#include <memory> #include <minecraft/auth/AuthSession.h> #include "tasks/Task.h" -typedef std::shared_ptr<class SkinDelete> SkinDeletePtr; +typedef shared_qobject_ptr<class SkinDelete> SkinDeletePtr; class SkinDelete : public Task { @@ -17,7 +16,7 @@ public: private: AuthSessionPtr m_session; - std::shared_ptr<QNetworkReply> m_reply; + shared_qobject_ptr<QNetworkReply> m_reply; protected: virtual void executeTask(); diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index 4e5a1698..e58d32d7 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -1,7 +1,9 @@ #include "SkinUpload.h" + #include <QNetworkRequest> #include <QHttpMultiPart> -#include <Env.h> + +#include "Application.h" QByteArray getVariant(SkinUpload::Model model) { switch (model) { @@ -37,8 +39,8 @@ void SkinUpload::executeTask() multiPart->append(skin); multiPart->append(model); - QNetworkReply *rep = ENV.qnam().post(request, multiPart); - m_reply = std::shared_ptr<QNetworkReply>(rep); + QNetworkReply *rep = APPLICATION->network()->post(request, multiPart); + m_reply = shared_qobject_ptr<QNetworkReply>(rep); setStatus(tr("Uploading skin")); connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h index ec859699..2c782e11 100644 --- a/launcher/minecraft/services/SkinUpload.h +++ b/launcher/minecraft/services/SkinUpload.h @@ -6,7 +6,7 @@ #include <minecraft/auth/AuthSession.h> #include "tasks/Task.h" -typedef std::shared_ptr<class SkinUpload> SkinUploadPtr; +typedef shared_qobject_ptr<class SkinUpload> SkinUploadPtr; class SkinUpload : public Task { @@ -26,7 +26,7 @@ private: Model m_model; QByteArray m_skin; AuthSessionPtr m_session; - std::shared_ptr<QNetworkReply> m_reply; + shared_qobject_ptr<QNetworkReply> m_reply; protected: virtual void executeTask(); diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index e26ab4ef..096e1719 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -1,10 +1,12 @@ -#include "Env.h" #include "AssetUpdateTask.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/ChecksumValidator.h" #include "minecraft/AssetsUtils.h" +#include "Application.h" + AssetUpdateTask::AssetUpdateTask(MinecraftInstance * inst) { m_inst = inst; @@ -24,7 +26,7 @@ void AssetUpdateTask::executeTask() QString localPath = assets->id + ".json"; auto job = new NetJob(tr("Asset index for %1").arg(m_inst->name())); - auto metacache = ENV.metacache(); + auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", localPath); entry->setStale(true); auto hexSha1 = assets->sha1.toLatin1(); @@ -41,7 +43,7 @@ void AssetUpdateTask::executeTask() connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); qDebug() << m_inst->name() << ": Starting asset index download"; - downloadJob->start(); + downloadJob->start(APPLICATION->network()); } bool AssetUpdateTask::canAbort() const @@ -62,7 +64,7 @@ void AssetUpdateTask::assetIndexFinished() // FIXME: this looks like a job for a generic validator based on json schema? if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) { - auto metacache = ENV.metacache(); + auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); metacache->evictEntry(entry); emitFailed(tr("Failed to read the assets index!")); @@ -76,7 +78,7 @@ void AssetUpdateTask::assetIndexFinished() connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); - downloadJob->start(); + downloadJob->start(APPLICATION->network()); return; } emitSucceeded(); diff --git a/launcher/minecraft/update/AssetUpdateTask.h b/launcher/minecraft/update/AssetUpdateTask.h index fdfa8f1c..6d7356f3 100644 --- a/launcher/minecraft/update/AssetUpdateTask.h +++ b/launcher/minecraft/update/AssetUpdateTask.h @@ -24,5 +24,5 @@ public slots: private: MinecraftInstance *m_inst; - NetJobPtr downloadJob; + NetJob::Ptr downloadJob; }; diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp index 8f1a43ff..a5c6b1e3 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -1,10 +1,12 @@ -#include "Env.h" -#include <FileSystem.h> -#include <minecraft/VersionFilterData.h> #include "FMLLibrariesTask.h" + +#include "FileSystem.h" +#include "minecraft/VersionFilterData.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" + #include "BuildConfig.h" +#include "Application.h" FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance * inst) { @@ -60,7 +62,7 @@ void FMLLibrariesTask::executeTask() // download missing libs to our place setStatus(tr("Downloading FML libraries...")); auto dljob = new NetJob("FML libraries"); - auto metacache = ENV.metacache(); + auto metacache = APPLICATION->metacache(); for (auto &lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); @@ -72,7 +74,7 @@ void FMLLibrariesTask::executeTask() connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress); downloadJob.reset(dljob); - downloadJob->start(); + downloadJob->start(APPLICATION->network()); } bool FMLLibrariesTask::canAbort() const @@ -87,7 +89,7 @@ void FMLLibrariesTask::fmllibsFinished() { setStatus(tr("Copying FML libraries into the instance...")); MinecraftInstance *inst = (MinecraftInstance *)m_inst; - auto metacache = ENV.metacache(); + auto metacache = APPLICATION->metacache(); int index = 0; for (auto &lib : fmlLibsToProcess) { diff --git a/launcher/minecraft/update/FMLLibrariesTask.h b/launcher/minecraft/update/FMLLibrariesTask.h index a1e70ed4..2e5ad83a 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.h +++ b/launcher/minecraft/update/FMLLibrariesTask.h @@ -25,7 +25,7 @@ public slots: private: MinecraftInstance *m_inst; - NetJobPtr downloadJob; + NetJob::Ptr downloadJob; QList<FMLlib> fmlLibsToProcess; }; diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 7f66a651..065b4e06 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -1,8 +1,10 @@ -#include "Env.h" #include "LibrariesTask.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "Application.h" + LibrariesTask::LibrariesTask(MinecraftInstance * inst) { m_inst = inst; @@ -21,7 +23,7 @@ void LibrariesTask::executeTask() auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); downloadJob.reset(job); - auto metacache = ENV.metacache(); + auto metacache = APPLICATION->metacache(); auto processArtifactPool = [&](const QList<LibraryPtr> & pool, QStringList & errors, const QString & localPath) { @@ -63,7 +65,7 @@ void LibrariesTask::executeTask() connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); - downloadJob->start(); + downloadJob->start(APPLICATION->network()); } bool LibrariesTask::canAbort() const diff --git a/launcher/minecraft/update/LibrariesTask.h b/launcher/minecraft/update/LibrariesTask.h index 49f76932..b966ad6c 100644 --- a/launcher/minecraft/update/LibrariesTask.h +++ b/launcher/minecraft/update/LibrariesTask.h @@ -22,5 +22,5 @@ public slots: private: MinecraftInstance *m_inst; - NetJobPtr downloadJob; + NetJob::Ptr downloadJob; }; |