From 49f8aae87736bfed1d1250ff2f24716adf3d1cc2 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 19 Apr 2024 20:29:28 -0400 Subject: [PATCH] Add support for installing the official benchmark This uses the same profile system as the regular game, and can be used to download the current benchmark (currently hardcoded, to be fixed later.) Or as always, install it offline from an existing zip. --- launcher/CMakeLists.txt | 3 + launcher/include/benchmarkinstaller.h | 35 ++++++ launcher/include/gamerunner.h | 8 +- launcher/include/launchercore.h | 3 + launcher/include/profile.h | 8 ++ launcher/profileconfig.kcfg | 3 + launcher/src/benchmarkinstaller.cpp | 89 +++++++++++++++ launcher/src/gamerunner.cpp | 108 +++++++++++++----- launcher/src/launchercore.cpp | 65 ++++++++--- launcher/src/profile.cpp | 31 +++++ launcher/ui/Main.qml | 2 +- launcher/ui/Pages/LoginPage.qml | 22 ++++ launcher/ui/Settings/AccountsPage.qml | 7 +- launcher/ui/Settings/ProfilesPage.qml | 1 + .../ui/Setup/BenchmarkInstallProgress.qml | 51 +++++++++ launcher/ui/Setup/SetupPage.qml | 52 ++++++++- 16 files changed, 430 insertions(+), 58 deletions(-) create mode 100644 launcher/include/benchmarkinstaller.h create mode 100644 launcher/src/benchmarkinstaller.cpp create mode 100644 launcher/ui/Setup/BenchmarkInstallProgress.qml diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 9351f31..ae3c9d7 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -44,6 +44,7 @@ target_sources(astra PRIVATE include/account.h include/accountmanager.h include/assetupdater.h + include/benchmarkinstaller.h include/compatibilitytoolinstaller.h include/encryptedarg.h include/gamerunner.h @@ -64,6 +65,7 @@ target_sources(astra PRIVATE src/account.cpp src/accountmanager.cpp src/assetupdater.cpp + src/benchmarkinstaller.cpp src/compatibilitytoolinstaller.cpp src/encryptedarg.cpp src/gamerunner.cpp @@ -105,6 +107,7 @@ qt_target_qml_sources(astra ui/Setup/AccountSetup.qml ui/Setup/AddSapphire.qml ui/Setup/AddSquareEnix.qml + ui/Setup/BenchmarkInstallProgress.qml ui/Setup/ExistingSetup.qml ui/Setup/InstallProgress.qml ui/Setup/SetupPage.qml diff --git a/launcher/include/benchmarkinstaller.h b/launcher/include/benchmarkinstaller.h new file mode 100644 index 0000000..197976a --- /dev/null +++ b/launcher/include/benchmarkinstaller.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +class LauncherCore; +class Profile; + +class BenchmarkInstaller : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("Use LauncherCore.createBenchmarkInstaller") + +public: + BenchmarkInstaller(LauncherCore &launcher, Profile &profile, QObject *parent = nullptr); + BenchmarkInstaller(LauncherCore &launcher, Profile &profile, const QString &filePath, QObject *parent = nullptr); + + Q_INVOKABLE void start(); + +Q_SIGNALS: + void installFinished(); + void error(QString message); + +private: + void installGame(); + + LauncherCore &m_launcher; + Profile &m_profile; + QString m_localInstallerPath; +}; \ No newline at end of file diff --git a/launcher/include/gamerunner.h b/launcher/include/gamerunner.h index 57b8f88..a680067 100644 --- a/launcher/include/gamerunner.h +++ b/launcher/include/gamerunner.h @@ -16,17 +16,17 @@ public: explicit GameRunner(LauncherCore &launcher, QObject *parent = nullptr); /// Begins the game executable, but calls to Dalamud if needed. - void beginGameExecutable(Profile &settings, const LoginAuth &auth); + void beginGameExecutable(Profile &settings, const std::optional &auth); private: /// Starts a vanilla game session with no Dalamud injection. - void beginVanillaGame(const QString &gameExecutablePath, Profile &profile, const LoginAuth &auth); + void beginVanillaGame(const QString &gameExecutablePath, Profile &profile, const std::optional &auth); /// Starts a game session with Dalamud injected. - void beginDalamudGame(const QString &gameExecutablePath, Profile &profile, const LoginAuth &auth); + void beginDalamudGame(const QString &gameExecutablePath, Profile &profile, const std::optional &auth); /// Returns the game arguments needed to properly launch the game. This encrypts it too if needed, and it's already joined! - QString getGameArgs(const Profile &profile, const LoginAuth &auth); + QString getGameArgs(const Profile &profile, const std::optional &auth); /// This wraps it in wine if needed. void launchExecutable(const Profile &settings, QProcess *process, const QStringList &args, bool isGame, bool needsRegistrySetup); diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h index 374fe32..f55d53b 100755 --- a/launcher/include/launchercore.h +++ b/launcher/include/launchercore.h @@ -22,6 +22,7 @@ class AssetUpdater; class GameInstaller; class CompatibilityToolInstaller; class GameRunner; +class BenchmarkInstaller; class LoginInformation : public QObject { @@ -88,6 +89,8 @@ public: Q_INVOKABLE GameInstaller *createInstaller(Profile *profile); Q_INVOKABLE GameInstaller *createInstallerFromExisting(Profile *profile, const QString &filePath); Q_INVOKABLE CompatibilityToolInstaller *createCompatInstaller(); + Q_INVOKABLE BenchmarkInstaller *createBenchmarkInstaller(Profile *profile); + Q_INVOKABLE BenchmarkInstaller *createBenchmarkInstallerFromExisting(Profile *profile, const QString &filePath); Q_INVOKABLE void clearAvatarCache(); Q_INVOKABLE void refreshNews(); diff --git a/launcher/include/profile.h b/launcher/include/profile.h index f9a43fe..112d6a6 100644 --- a/launcher/include/profile.h +++ b/launcher/include/profile.h @@ -42,6 +42,8 @@ class Profile : public QObject Q_PROPERTY(QString dalamudVersionText READ dalamudVersionText NOTIFY gameInstallChanged) Q_PROPERTY(QString wineVersionText READ wineVersionText NOTIFY wineChanged) Q_PROPERTY(bool loggedIn READ loggedIn NOTIFY loggedInChanged) + Q_PROPERTY(bool isBenchmark READ isBenchmark WRITE setIsBenchmark NOTIFY isBenchmarkChanged) + Q_PROPERTY(QString subtitle READ subtitle NOTIFY gameInstallChanged) public: explicit Profile(LauncherCore &launcher, const QString &key, QObject *parent = nullptr); @@ -110,6 +112,9 @@ public: [[nodiscard]] int dalamudInjectDelay() const; void setDalamudInjectDelay(int value); + [[nodiscard]] bool isBenchmark() const; + void setIsBenchmark(bool value); + [[nodiscard]] Account *account() const; [[nodiscard]] QString accountUuid() const; void setAccount(Account *account); @@ -147,6 +152,8 @@ public: [[nodiscard]] bool loggedIn() const; void setLoggedIn(bool value); + [[nodiscard]] QString subtitle() const; + Q_SIGNALS: void gameInstallChanged(); void nameChanged(); @@ -166,6 +173,7 @@ Q_SIGNALS: void dalamudChannelChanged(); void dalamudInjectMethodChanged(); void dalamudInjectDelayChanged(); + void isBenchmarkChanged(); void accountChanged(); void wineChanged(); void loggedInChanged(); diff --git a/launcher/profileconfig.kcfg b/launcher/profileconfig.kcfg index b85d9a0..53c3a53 100644 --- a/launcher/profileconfig.kcfg +++ b/launcher/profileconfig.kcfg @@ -83,5 +83,8 @@ SPDX-License-Identifier: CC0-1.0 0 + + false + diff --git a/launcher/src/benchmarkinstaller.cpp b/launcher/src/benchmarkinstaller.cpp new file mode 100644 index 0000000..1717fc3 --- /dev/null +++ b/launcher/src/benchmarkinstaller.cpp @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "benchmarkinstaller.h" + +#include +#include +#include +#include +#include +#include + +#include "astra_log.h" +#include "launchercore.h" +#include "profile.h" +#include "utility.h" + +// TODO: this should be dynamically grabbed from the webpage to avoid hardcoding it +const QString installerUrl = QStringLiteral("https://download.finalfantasyxiv.com/s9qmq6SJfMMqYM4o/ffxiv-dawntrail-bench.zip"); + +BenchmarkInstaller::BenchmarkInstaller(LauncherCore &launcher, Profile &profile, QObject *parent) + : QObject(parent) + , m_launcher(launcher) + , m_profile(profile) +{ +} + +BenchmarkInstaller::BenchmarkInstaller(LauncherCore &launcher, Profile &profile, const QString &filePath, QObject *parent) + : BenchmarkInstaller(launcher, profile, parent) +{ + m_localInstallerPath = filePath; +} + +void BenchmarkInstaller::start() +{ + if (m_localInstallerPath.isEmpty()) { + const QNetworkRequest request = QNetworkRequest(QUrl(installerUrl)); + Utility::printRequest(QStringLiteral("GET"), request); + + // TODO: benchmarks are usually quite large, and need download progress reporting + auto reply = m_launcher.mgr()->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply] { + if (reply->error() != QNetworkReply::NetworkError::NoError) { + Q_EMIT error(reply->errorString()); + return; + } + + const QDir dataDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + + const QByteArray data = reply->readAll(); + + QFile file(dataDir.absoluteFilePath(QStringLiteral("ffxiv-bench.zip"))); + file.open(QIODevice::WriteOnly); + file.write(data); + file.close(); + + m_localInstallerPath = file.fileName(); + installGame(); + }); + } else { + installGame(); + } +} + +void BenchmarkInstaller::installGame() +{ + const QDir installDirectory = m_profile.gamePath(); + + KZip archive(m_localInstallerPath); + if (!archive.open(QIODevice::ReadOnly)) { + qCritical(ASTRA_LOG) << "Failed to extract benchmark files"; + Q_EMIT error(i18n("Failed to extract benchmark files.")); + return; + } + + // the first directory is the same as the version we download + const KArchiveDirectory *root = archive.directory(); + root->copyTo(installDirectory.absolutePath(), true); + + archive.close(); + + m_profile.readGameVersion(); + + Q_EMIT installFinished(); + qInfo(ASTRA_LOG) << "Installed game in" << installDirectory; +} + +#include "moc_benchmarkinstaller.cpp" \ No newline at end of file diff --git a/launcher/src/gamerunner.cpp b/launcher/src/gamerunner.cpp index 09a7b7c..885232f 100644 --- a/launcher/src/gamerunner.cpp +++ b/launcher/src/gamerunner.cpp @@ -20,7 +20,7 @@ GameRunner::GameRunner(LauncherCore &launcher, QObject *parent) { } -void GameRunner::beginGameExecutable(Profile &profile, const LoginAuth &auth) +void GameRunner::beginGameExecutable(Profile &profile, const std::optional &auth) { QString gameExectuable; if (profile.directx9Enabled() && profile.hasDirectx9()) { @@ -38,7 +38,7 @@ void GameRunner::beginGameExecutable(Profile &profile, const LoginAuth &auth) Q_EMIT m_launcher.successfulLaunch(); } -void GameRunner::beginVanillaGame(const QString &gameExecutablePath, Profile &profile, const LoginAuth &auth) +void GameRunner::beginVanillaGame(const QString &gameExecutablePath, Profile &profile, const std::optional &auth) { profile.setLoggedIn(true); @@ -57,7 +57,7 @@ void GameRunner::beginVanillaGame(const QString &gameExecutablePath, Profile &pr launchExecutable(profile, gameProcess, {gameExecutablePath, args}, true, true); } -void GameRunner::beginDalamudGame(const QString &gameExecutablePath, Profile &profile, const LoginAuth &auth) +void GameRunner::beginDalamudGame(const QString &gameExecutablePath, Profile &profile, const std::optional &auth) { profile.setLoggedIn(true); @@ -123,44 +123,84 @@ void GameRunner::beginDalamudGame(const QString &gameExecutablePath, Profile &pr true); } -QString GameRunner::getGameArgs(const Profile &profile, const LoginAuth &auth) +QString GameRunner::getGameArgs(const Profile &profile, const std::optional &auth) { QList> gameArgs; - gameArgs.push_back({QStringLiteral("DEV.DataPathType"), QString::number(1)}); - gameArgs.push_back({QStringLiteral("DEV.UseSqPack"), QString::number(1)}); - gameArgs.push_back({QStringLiteral("DEV.MaxEntitledExpansionID"), QString::number(auth.maxExpansion)}); - gameArgs.push_back({QStringLiteral("DEV.TestSID"), auth.SID}); - gameArgs.push_back({QStringLiteral("SYS.Region"), QString::number(auth.region)}); - gameArgs.push_back({QStringLiteral("language"), QString::number(profile.account()->language())}); - gameArgs.push_back({QStringLiteral("ver"), profile.baseGameVersion()}); - gameArgs.push_back({QStringLiteral("UserPath"), Utility::toWindowsPath(profile.account()->getConfigDir().absolutePath())}); - gameArgs.push_back({QStringLiteral("resetconfig"), QStringLiteral("0")}); + if (!profile.isBenchmark()) { + gameArgs.push_back({QStringLiteral("DEV.DataPathType"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("DEV.UseSqPack"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("ver"), profile.baseGameVersion()}); + gameArgs.push_back({QStringLiteral("resetconfig"), QStringLiteral("0")}); + } + + if (auth) { + gameArgs.push_back({QStringLiteral("DEV.MaxEntitledExpansionID"), QString::number(auth->maxExpansion)}); + gameArgs.push_back({QStringLiteral("DEV.TestSID"), auth->SID}); + gameArgs.push_back({QStringLiteral("SYS.Region"), QString::number(auth->region)}); + gameArgs.push_back({QStringLiteral("language"), QString::number(profile.account()->language())}); + gameArgs.push_back({QStringLiteral("UserPath"), Utility::toWindowsPath(profile.account()->getConfigDir().absolutePath())}); + + Utility::createPathIfNeeded(profile.account()->getConfigDir().absolutePath()); + } else if (profile.isBenchmark()) { + gameArgs.push_back({QStringLiteral("SYS.Language"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.Fps"), QString::number(0)}); + gameArgs.push_back({QStringLiteral("SYS.WaterWet_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.OcclusionCulling_DX11"), QString::number(0)}); + gameArgs.push_back({QStringLiteral("SYS.LodType_DX11"), QString::number(0)}); + gameArgs.push_back({QStringLiteral("SYS.ReflectionType_DX11"), QString::number(3)}); + gameArgs.push_back({QStringLiteral("SYS.AntiAliasing_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.TranslucentQuality_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.GrassQuality_DX11"), QString::number(3)}); + gameArgs.push_back({QStringLiteral("SYS.ShadowLOD_DX11"), QString::number(0)}); + gameArgs.push_back({QStringLiteral("SYS.ShadowVisibilityTypeSelf_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.ShadowVisibilityTypeOther_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.ShadowTextureSizeType_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.ShadowCascadeCountType_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.ShadowSoftShadowType_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.PhysicsTypeSelf_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.PhysicsTypeOther_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.TextureFilterQuality_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.TextureAnisotropicQuality_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.Vignetting_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.RadialBlur_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.SSAO_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.Glare_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.DepthOfField_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.ParallaxOcclusion_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.Tessellation_DX11"), QString::number(0)}); + gameArgs.push_back({QStringLiteral("SYS.GlareRepresentation_DX11"), QString::number(1)}); + gameArgs.push_back({QStringLiteral("SYS.DistortionWater_DX11"), QString::number(2)}); + gameArgs.push_back({QStringLiteral("SYS.ScreenMode"), QString::number(0)}); + gameArgs.push_back({QStringLiteral("SYS.ScreenWidth"), QString::number(1920)}); + gameArgs.push_back({QStringLiteral("SYS.ScreenHeight"), QString::number(1080)}); + } // FIXME: this should belong somewhere else... - Utility::createPathIfNeeded(profile.account()->getConfigDir().absolutePath()); Utility::createPathIfNeeded(profile.winePrefixPath()); - if (!auth.lobbyhost.isEmpty()) { - gameArgs.push_back({QStringLiteral("DEV.GMServerHost"), auth.frontierHost}); - for (int i = 1; i < 9; i++) { - gameArgs.push_back({QStringLiteral("DEV.LobbyHost0%1").arg(QString::number(i)), auth.lobbyhost}); - gameArgs.push_back({QStringLiteral("DEV.LobbyPort0%1").arg(QString::number(i)), QString::number(54994)}); + if (auth) { + if (!auth->lobbyhost.isEmpty()) { + gameArgs.push_back({QStringLiteral("DEV.GMServerHost"), auth->frontierHost}); + for (int i = 1; i < 9; i++) { + gameArgs.push_back({QStringLiteral("DEV.LobbyHost0%1").arg(QString::number(i)), auth->lobbyhost}); + gameArgs.push_back({QStringLiteral("DEV.LobbyPort0%1").arg(QString::number(i)), QString::number(54994)}); + } + } + + if (profile.account()->license() == Account::GameLicense::WindowsSteam) { + gameArgs.push_back({QStringLiteral("IsSteam"), QString::number(1)}); } } - if (profile.account()->license() == Account::GameLicense::WindowsSteam) { - gameArgs.push_back({QStringLiteral("IsSteam"), QString::number(1)}); - } - - const QString argFormat = m_launcher.settings()->argumentsEncrypted() ? QStringLiteral(" /%1 =%2") : QStringLiteral(" %1=%2"); + const QString argFormat = !profile.isBenchmark() && m_launcher.settings()->argumentsEncrypted() ? QStringLiteral(" /%1 =%2") : QStringLiteral(" %1=%2"); QString argJoined; for (const auto &[key, value] : gameArgs) { argJoined += argFormat.arg(key, value); } - return m_launcher.settings()->argumentsEncrypted() ? encryptGameArg(argJoined) : argJoined; + return !profile.isBenchmark() && m_launcher.settings()->argumentsEncrypted() ? encryptGameArg(argJoined) : argJoined; } void GameRunner::launchExecutable(const Profile &profile, QProcess *process, const QStringList &args, bool isGame, bool needsRegistrySetup) @@ -171,8 +211,10 @@ void GameRunner::launchExecutable(const Profile &profile, QProcess *process, con if (needsRegistrySetup) { #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) // FFXIV detects this as a "macOS" build by checking if Wine shows up - const int value = profile.account()->license() == Account::GameLicense::macOS ? 0 : 1; - addRegistryKey(profile, QStringLiteral("HKEY_CURRENT_USER\\Software\\Wine"), QStringLiteral("HideWineExports"), QString::number(value)); + if (!profile.isBenchmark()) { + const int value = profile.account()->license() == Account::GameLicense::macOS ? 0 : 1; + addRegistryKey(profile, QStringLiteral("HKEY_CURRENT_USER\\Software\\Wine"), QStringLiteral("HideWineExports"), QString::number(value)); + } setWindowsVersion(profile, QStringLiteral("win7")); @@ -256,7 +298,7 @@ void GameRunner::launchExecutable(const Profile &profile, QProcess *process, con arguments.push_back(profile.winePath()); #endif - if (profile.account()->license() == Account::GameLicense::WindowsSteam) { + if (!profile.isBenchmark() && profile.account()->license() == Account::GameLicense::WindowsSteam) { env.insert(QStringLiteral("IS_FFXIV_LAUNCH_FROM_STEAM"), QStringLiteral("1")); } @@ -264,8 +306,14 @@ void GameRunner::launchExecutable(const Profile &profile, QProcess *process, con const QString executable = arguments.takeFirst(); - if (isGame) - process->setWorkingDirectory(profile.gamePath() + QStringLiteral("/game/")); + if (isGame) { + if (profile.isBenchmark()) { + // Benchmarks usually have some data located in the root + process->setWorkingDirectory(profile.gamePath()); + } else { + process->setWorkingDirectory(profile.gamePath() + QStringLiteral("/game/")); + } + } process->setProcessEnvironment(env); diff --git a/launcher/src/launchercore.cpp b/launcher/src/launchercore.cpp index cd7828c..de2a340 100755 --- a/launcher/src/launchercore.cpp +++ b/launcher/src/launchercore.cpp @@ -14,6 +14,7 @@ #include "account.h" #include "assetupdater.h" #include "astra_log.h" +#include "benchmarkinstaller.h" #include "compatibilitytoolinstaller.h" #include "gamerunner.h" #include "launchercore.h" @@ -65,12 +66,16 @@ void LauncherCore::login(Profile *profile, const QString &username, const QStrin auto loginInformation = new LoginInformation(this); loginInformation->profile = profile; - loginInformation->username = username; - loginInformation->password = password; - loginInformation->oneTimePassword = oneTimePassword; - if (profile->account()->rememberPassword()) { - profile->account()->setPassword(password); + // Benchmark never has to login, of course + if (!profile->isBenchmark()) { + loginInformation->username = username; + loginInformation->password = password; + loginInformation->oneTimePassword = oneTimePassword; + + if (profile->account()->rememberPassword()) { + profile->account()->setPassword(password); + } } beginLogin(*loginInformation); @@ -117,6 +122,20 @@ CompatibilityToolInstaller *LauncherCore::createCompatInstaller() return new CompatibilityToolInstaller(*this, this); } +BenchmarkInstaller *LauncherCore::createBenchmarkInstaller(Profile *profile) +{ + Q_ASSERT(profile != nullptr); + + return new BenchmarkInstaller(*this, *profile, this); +} + +BenchmarkInstaller *LauncherCore::createBenchmarkInstallerFromExisting(Profile *profile, const QString &filePath) +{ + Q_ASSERT(profile != nullptr); + + return new BenchmarkInstaller(*this, *profile, filePath, this); +} + void LauncherCore::clearAvatarCache() { const QString cacheLocation = QStandardPaths::standardLocations(QStandardPaths::CacheLocation)[0] + QStringLiteral("/avatars"); @@ -320,26 +339,34 @@ QString LauncherCore::cachedLogoImage() const QCoro::Task<> LauncherCore::beginLogin(LoginInformation &info) { - info.profile->account()->updateConfig(); + // Hmm, I don't think we're set up for this yet? + if (!info.profile->isBenchmark()) { + info.profile->account()->updateConfig(); + } auto assetUpdater = new AssetUpdater(*info.profile, *this, this); if (co_await assetUpdater->update()) { std::optional auth; - if (info.profile->account()->isSapphire()) { - auth = co_await m_sapphireLogin->login(info.profile->account()->lobbyUrl(), info); - } else { - auth = co_await m_squareEnixLogin->login(&info); - } - - if (auth != std::nullopt) { - Q_EMIT stageChanged(i18n("Launching game...")); - - if (isSteam()) { - m_steamApi->setLauncherMode(false); + if (!info.profile->isBenchmark()) { + if (info.profile->account()->isSapphire()) { + auth = co_await m_sapphireLogin->login(info.profile->account()->lobbyUrl(), info); + } else { + auth = co_await m_squareEnixLogin->login(&info); } - - m_runner->beginGameExecutable(*info.profile, *auth); } + + // If we expect an auth ticket, don't continue if missing + if (!info.profile->isBenchmark() && auth == std::nullopt) { + co_return; + } + + Q_EMIT stageChanged(i18n("Launching game...")); + + if (isSteam()) { + m_steamApi->setLauncherMode(false); + } + + m_runner->beginGameExecutable(*info.profile, auth); } assetUpdater->deleteLater(); diff --git a/launcher/src/profile.cpp b/launcher/src/profile.cpp index 142b546..05a8f28 100644 --- a/launcher/src/profile.cpp +++ b/launcher/src/profile.cpp @@ -371,6 +371,20 @@ void Profile::setDalamudInjectDelay(const int value) } } +bool Profile::isBenchmark() const +{ + return m_config->isBenchmark(); +} + +void Profile::setIsBenchmark(bool value) +{ + if (m_config->isBenchmark() != value) { + m_config->setIsBenchmark(value); + m_config->save(); + Q_EMIT isBenchmarkChanged(); + } +} + Account *Profile::account() const { return m_account; @@ -575,4 +589,21 @@ void Profile::setLoggedIn(const bool value) } } +QString Profile::subtitle() const +{ + if (isBenchmark()) { + return i18n("Benchmark"); + } else if (m_repositories.repositories_count > 0) { + const unsigned int latestExpansion = m_repositories.repositories_count - 1; + QString expansionName = i18n("Unknown Expansion"); + if (latestExpansion < static_cast(m_expansionNames.size())) { + expansionName = m_expansionNames[latestExpansion]; + } + + return QStringLiteral("%1 (%2)").arg(expansionName, QString::fromLatin1(m_repositories.repositories[latestExpansion].version)); + } else { + return i18n("Unknown"); + } +} + #include "moc_profile.cpp" \ No newline at end of file diff --git a/launcher/ui/Main.qml b/launcher/ui/Main.qml index 1c91e22..1b42c1a 100644 --- a/launcher/ui/Main.qml +++ b/launcher/ui/Main.qml @@ -58,7 +58,7 @@ Kirigami.ApplicationWindow { pageStack.push(Qt.createComponent("zone.xiv.astra", "SetupPage"), { profile: LauncherCore.currentProfile }) - } else if (!LauncherCore.currentProfile.account) { + } else if (!LauncherCore.currentProfile.account && !LauncherCore.currentProfile.isBenchmark) { // User must select an account for the profile pageStack.push(Qt.createComponent("zone.xiv.astra", "AccountSetup"), { profile: LauncherCore.currentProfile diff --git a/launcher/ui/Pages/LoginPage.qml b/launcher/ui/Pages/LoginPage.qml index 7ab06e9..83cef58 100644 --- a/launcher/ui/Pages/LoginPage.qml +++ b/launcher/ui/Pages/LoginPage.qml @@ -158,6 +158,9 @@ QQC2.Control { } FormCard.FormCard { + id: regularLoginCard + + visible: !LauncherCore.currentProfile.isBenchmark maximumWidth: Kirigami.Units.gridUnit * 25 Layout.fillWidth: true @@ -296,6 +299,25 @@ QQC2.Control { } } + FormCard.FormCard { + id: benchmarkLaunchCard + + visible: LauncherCore.currentProfile.isBenchmark + maximumWidth: Kirigami.Units.gridUnit * 25 + + Layout.fillWidth: true + + FormCard.FormButtonDelegate { + id: launchBenchmarkDelegate + + text: i18n("Launch Benchmark") + onClicked: { + LauncherCore.login(LauncherCore.currentProfile, "", "", "") + page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "StatusPage")) + } + } + } + Item { Layout.fillHeight: true } diff --git a/launcher/ui/Settings/AccountsPage.qml b/launcher/ui/Settings/AccountsPage.qml index 8799e54..580736e 100644 --- a/launcher/ui/Settings/AccountsPage.qml +++ b/launcher/ui/Settings/AccountsPage.qml @@ -35,10 +35,12 @@ FormCard.FormCardPage { ] FormCard.FormCard { - Layout.fillWidth: true Layout.topMargin: Kirigami.Units.largeSpacing + visible: repeater.count !== 0 Repeater { + id: repeater + model: LauncherCore.accountManager ColumnLayout { @@ -53,7 +55,8 @@ FormCard.FormCardPage { text: layout.account.name description: layout.account.isSapphire ? i18n("Sapphire") : i18n("Square Enix") - leading: Components.Avatar { + leading: Components.Avatar + { name: layout.account.name source: layout.account.avatarUrl } diff --git a/launcher/ui/Settings/ProfilesPage.qml b/launcher/ui/Settings/ProfilesPage.qml index 5f26791..0ae8ac2 100644 --- a/launcher/ui/Settings/ProfilesPage.qml +++ b/launcher/ui/Settings/ProfilesPage.qml @@ -88,6 +88,7 @@ FormCard.FormCardPage { id: buttonDelegate text: layout.profile.name + description: layout.profile.subtitle onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "ProfileSettings"), { profile: layout.profile }) diff --git a/launcher/ui/Setup/BenchmarkInstallProgress.qml b/launcher/ui/Setup/BenchmarkInstallProgress.qml new file mode 100644 index 0000000..13d1db3 --- /dev/null +++ b/launcher/ui/Setup/BenchmarkInstallProgress.qml @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import zone.xiv.astra + +Kirigami.Page { + id: page + + property var benchmarkInstaller + + title: i18n("Benchmark Installation") + + Kirigami.LoadingPlaceholder { + anchors.centerIn: parent + + text: i18n("Downloading…") + } + + Kirigami.PromptDialog { + id: errorDialog + title: i18n("Installation Error") + + showCloseButton: false + standardButtons: Kirigami.Dialog.Ok + + onAccepted: page.Window.window.pageStack.layers.pop() + onRejected: page.Window.window.pageStack.layers.pop() + } + + Component.onCompleted: benchmarkInstaller.start() + + Connections { + target: page.benchmarkInstaller + + function onInstallFinished() { + // Prevents it from failing to push the page if the install happens too quickly. + Qt.callLater(() => applicationWindow().checkSetup()); + } + + function onError(message) { + errorDialog.subtitle = i18n("An error has occurred while installing the benchmark:\n\n%1", message); + errorDialog.open(); + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/SetupPage.qml b/launcher/ui/Setup/SetupPage.qml index fd51b4c..c6f4204 100644 --- a/launcher/ui/Setup/SetupPage.qml +++ b/launcher/ui/Setup/SetupPage.qml @@ -97,8 +97,7 @@ FormCard.FormCardPage { } FormCard.FormHeader { - title: i18n("Install Game") - visible: LauncherCore.accountManager.hasAnyAccounts() + title: i18n("Retail Game") } FormCard.FormCard { @@ -145,4 +144,53 @@ FormCard.FormCardPage { onClicked: dialog.open() } } + + FormCard.FormHeader { + title: i18n("Benchmark") + } + + FormCard.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + + FormCard.FormButtonDelegate { + id: downloadBenchmark + + text: i18n("Download & Install Benchmark") + description: i18n("Download the official benchmark from Square Enix.") + icon.name: "cloud-download" + onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "BenchmarkInstallProgress"), { + benchmarkInstaller: LauncherCore.createBenchmarkInstaller(page.profile) + }) + } + + FormCard.FormDelegateSeparator { + above: downloadBenchmark + below: selectBenchmark + } + + FormCard.FormButtonDelegate { + id: selectBenchmark + + text: i18n("Select Existing Benchmark…") + description: i18n("Use a previously downloaded benchmark. Useful if offline or can't otherwise access the official servers.") + icon.name: "edit-find" + + FileDialog { + id: benchmarkDialog + + currentFolder: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] + nameFilters: [i18n("Benchmark zip archive (*.zip)")] + + onAccepted: { + const url = decodeURIComponent(selectedFile.toString().replace("file://", "")); + page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "BenchmarkInstallProgress"), { + benchmarkInstaller: LauncherCore.createBenchmarkInstallerFromExisting(page.profile, url) + }); + } + } + + onClicked: benchmarkDialog.open() + } + } } \ No newline at end of file