2023-08-05 22:14:05 -04:00
|
|
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
#include "gameinstaller.h"
|
|
|
|
|
2023-09-17 08:31:24 -04:00
|
|
|
#include <KLocalizedString>
|
2023-07-30 08:49:34 -04:00
|
|
|
#include <QDir>
|
2024-03-22 20:12:06 -04:00
|
|
|
#include <QImage>
|
2023-07-30 08:49:34 -04:00
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
#include <QStandardPaths>
|
|
|
|
#include <algorithm>
|
2023-09-16 18:37:42 -04:00
|
|
|
#include <qcoronetworkreply.h>
|
2023-07-30 08:49:34 -04:00
|
|
|
|
|
|
|
#include "account.h"
|
|
|
|
#include "assetupdater.h"
|
2023-10-08 18:02:02 -04:00
|
|
|
#include "astra_log.h"
|
2024-04-19 20:29:28 -04:00
|
|
|
#include "benchmarkinstaller.h"
|
2023-08-18 23:27:29 -04:00
|
|
|
#include "compatibilitytoolinstaller.h"
|
2023-10-11 14:30:21 -04:00
|
|
|
#include "gamerunner.h"
|
2023-07-30 08:49:34 -04:00
|
|
|
#include "launchercore.h"
|
2023-10-11 14:13:42 -04:00
|
|
|
#include "sapphirelogin.h"
|
|
|
|
#include "squareenixlogin.h"
|
2023-08-18 14:52:06 -04:00
|
|
|
#include "utility.h"
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-07-30 18:43:06 -04:00
|
|
|
#ifdef BUILD_SYNC
|
|
|
|
#include "charactersync.h"
|
|
|
|
#include "syncmanager.h"
|
|
|
|
#endif
|
|
|
|
|
2024-08-22 21:22:56 -04:00
|
|
|
#ifdef HAS_DBUS
|
|
|
|
#include <QDBusConnection>
|
|
|
|
#include <QDBusReply>
|
|
|
|
#include <QGuiApplication>
|
|
|
|
#endif
|
|
|
|
|
2023-12-17 12:01:28 -05:00
|
|
|
using namespace Qt::StringLiterals;
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
LauncherCore::LauncherCore()
|
|
|
|
: QObject()
|
2023-09-17 18:43:58 -04:00
|
|
|
{
|
2023-10-11 14:49:24 -04:00
|
|
|
m_settings = new LauncherSettings(this);
|
|
|
|
m_mgr = new QNetworkAccessManager(this);
|
|
|
|
m_sapphireLogin = new SapphireLogin(*this, this);
|
|
|
|
m_squareEnixLogin = new SquareEnixLogin(*this, this);
|
2024-08-22 18:23:06 -04:00
|
|
|
m_profileManager = new ProfileManager(this);
|
2024-08-22 18:53:46 -04:00
|
|
|
m_accountManager = new AccountManager(this);
|
2023-10-11 14:49:24 -04:00
|
|
|
m_runner = new GameRunner(*this, this);
|
2024-07-30 18:43:06 -04:00
|
|
|
|
2024-08-22 18:46:45 -04:00
|
|
|
connect(m_accountManager, &AccountManager::accountAdded, this, &LauncherCore::fetchAvatar);
|
|
|
|
connect(m_accountManager, &AccountManager::accountLodestoneIdChanged, this, &LauncherCore::fetchAvatar);
|
|
|
|
|
2024-07-30 19:47:26 -04:00
|
|
|
connect(this, &LauncherCore::gameClosed, this, &LauncherCore::handleGameExit);
|
|
|
|
|
2024-07-30 18:43:06 -04:00
|
|
|
#ifdef BUILD_SYNC
|
2024-07-28 11:17:30 -04:00
|
|
|
m_syncManager = new SyncManager(this);
|
2024-07-30 18:43:06 -04:00
|
|
|
#endif
|
2023-09-17 18:43:58 -04:00
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
m_profileManager->load();
|
|
|
|
m_accountManager->load();
|
|
|
|
|
|
|
|
// restore profile -> account connections
|
2024-07-04 20:53:06 -04:00
|
|
|
for (const auto profile : m_profileManager->profiles()) {
|
|
|
|
if (const auto account = m_accountManager->getByUuid(profile->accountUuid())) {
|
2023-07-30 08:49:34 -04:00
|
|
|
profile->setAccount(account);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-08 20:05:37 -04:00
|
|
|
// set default profile, if found
|
2024-07-04 20:53:06 -04:00
|
|
|
if (const auto profile = m_profileManager->getProfileByUUID(m_settings->currentProfile())) {
|
2023-10-08 20:05:37 -04:00
|
|
|
setCurrentProfile(profile);
|
|
|
|
}
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
m_loadingFinished = true;
|
|
|
|
Q_EMIT loadingFinished();
|
|
|
|
}
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
void LauncherCore::initializeSteam()
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-11 14:49:24 -04:00
|
|
|
m_steamApi = new SteamAPI(this);
|
|
|
|
m_steamApi->setLauncherMode(true);
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void LauncherCore::login(Profile *profile, const QString &username, const QString &password, const QString &oneTimePassword)
|
|
|
|
{
|
2023-09-17 18:43:58 -04:00
|
|
|
Q_ASSERT(profile != nullptr);
|
2023-07-30 16:19:51 -04:00
|
|
|
|
2024-08-22 21:22:56 -04:00
|
|
|
inhibitSleep();
|
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto loginInformation = new LoginInformation(this);
|
2023-09-17 18:43:58 -04:00
|
|
|
loginInformation->profile = profile;
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-04-19 20:29:28 -04:00
|
|
|
// 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);
|
|
|
|
}
|
2023-09-17 18:43:58 -04:00
|
|
|
}
|
2023-07-30 16:19:51 -04:00
|
|
|
|
2023-09-17 18:43:58 -04:00
|
|
|
beginLogin(*loginInformation);
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2023-10-08 13:21:13 -04:00
|
|
|
bool LauncherCore::autoLogin(Profile *profile)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-08 17:52:44 -04:00
|
|
|
Q_ASSERT(profile != nullptr);
|
|
|
|
|
2023-10-06 18:09:50 -04:00
|
|
|
QString otp;
|
|
|
|
if (profile->account()->useOTP()) {
|
|
|
|
if (!profile->account()->rememberOTP()) {
|
2023-10-08 18:02:02 -04:00
|
|
|
Q_EMIT loginError(i18n("This account does not have an OTP secret set, but requires it for login."));
|
2023-10-08 13:21:13 -04:00
|
|
|
return false;
|
2023-10-06 18:09:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
otp = profile->account()->getOTP();
|
|
|
|
if (otp.isEmpty()) {
|
2023-10-08 18:02:02 -04:00
|
|
|
Q_EMIT loginError(i18n("Failed to generate OTP, review the stored secret."));
|
2023-10-08 13:21:13 -04:00
|
|
|
return false;
|
2023-10-06 18:09:50 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
login(profile, profile->account()->name(), profile->account()->getPassword(), otp);
|
2023-10-08 13:21:13 -04:00
|
|
|
return true;
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2024-06-27 16:38:45 -04:00
|
|
|
void LauncherCore::immediatelyLaunch(Profile *profile)
|
|
|
|
{
|
|
|
|
Q_ASSERT(profile != nullptr);
|
|
|
|
|
|
|
|
m_runner->beginGameExecutable(*profile, std::nullopt);
|
|
|
|
}
|
|
|
|
|
2023-08-18 12:59:07 -04:00
|
|
|
GameInstaller *LauncherCore::createInstaller(Profile *profile)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-08-18 12:59:07 -04:00
|
|
|
Q_ASSERT(profile != nullptr);
|
2023-10-08 17:52:44 -04:00
|
|
|
|
2023-08-18 12:59:07 -04:00
|
|
|
return new GameInstaller(*this, *profile, this);
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2024-04-01 14:54:41 -04:00
|
|
|
GameInstaller *LauncherCore::createInstallerFromExisting(Profile *profile, const QString &filePath)
|
|
|
|
{
|
|
|
|
Q_ASSERT(profile != nullptr);
|
|
|
|
|
|
|
|
return new GameInstaller(*this, *profile, filePath, this);
|
|
|
|
}
|
|
|
|
|
2023-08-18 23:27:29 -04:00
|
|
|
CompatibilityToolInstaller *LauncherCore::createCompatInstaller()
|
|
|
|
{
|
|
|
|
return new CompatibilityToolInstaller(*this, this);
|
|
|
|
}
|
|
|
|
|
2024-04-19 20:29:28 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-08-22 18:46:45 -04:00
|
|
|
void LauncherCore::fetchAvatar(Account *account)
|
|
|
|
{
|
|
|
|
if (account->lodestoneId().isEmpty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString cacheLocation = QStandardPaths::standardLocations(QStandardPaths::CacheLocation)[0] + QStringLiteral("/avatars");
|
|
|
|
Utility::createPathIfNeeded(cacheLocation);
|
|
|
|
|
|
|
|
const QString filename = QStringLiteral("%1/%2.jpg").arg(cacheLocation, account->lodestoneId());
|
|
|
|
if (!QFile(filename).exists()) {
|
|
|
|
qDebug(ASTRA_LOG) << "Did not find lodestone character " << account->lodestoneId() << " in cache, fetching from Lodestone.";
|
|
|
|
|
|
|
|
QUrl url;
|
|
|
|
url.setScheme(settings()->preferredProtocol());
|
|
|
|
url.setHost(QStringLiteral("na.%1").arg(settings()->mainServer())); // TODO: NA isnt the only thing in the world...
|
|
|
|
url.setPath(QStringLiteral("/lodestone/character/%1").arg(account->lodestoneId()));
|
|
|
|
|
|
|
|
const QNetworkRequest request(url);
|
|
|
|
Utility::printRequest(QStringLiteral("GET"), request);
|
|
|
|
|
|
|
|
const auto reply = mgr()->get(request);
|
|
|
|
connect(reply, &QNetworkReply::finished, [this, filename, reply, account] {
|
|
|
|
const QString document = QString::fromUtf8(reply->readAll());
|
|
|
|
if (!document.isEmpty()) {
|
|
|
|
const static QRegularExpression re(
|
|
|
|
QStringLiteral(R"lit(<div\s[^>]*class=["|']frame__chara__face["|'][^>]*>\s*<img\s[&>]*src=["|']([^"']*))lit"));
|
|
|
|
const QRegularExpressionMatch match = re.match(document);
|
|
|
|
|
|
|
|
if (match.hasCaptured(1)) {
|
|
|
|
const QString newAvatarUrl = match.captured(1);
|
|
|
|
|
|
|
|
const auto avatarRequest = QNetworkRequest(QUrl(newAvatarUrl));
|
|
|
|
Utility::printRequest(QStringLiteral("GET"), avatarRequest);
|
|
|
|
|
|
|
|
auto avatarReply = mgr()->get(avatarRequest);
|
|
|
|
connect(avatarReply, &QNetworkReply::finished, [this, filename, avatarReply, account] {
|
|
|
|
QFile file(filename);
|
|
|
|
file.open(QIODevice::ReadWrite);
|
|
|
|
file.write(avatarReply->readAll());
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
account->setAvatarUrl(QStringLiteral("file:///%1").arg(filename));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
account->setAvatarUrl(QStringLiteral("file:///%1").arg(filename));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
void LauncherCore::clearAvatarCache()
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-12-17 12:01:28 -05:00
|
|
|
const QString cacheLocation = QStandardPaths::standardLocations(QStandardPaths::CacheLocation)[0] + QStringLiteral("/avatars");
|
2023-10-11 14:49:24 -04:00
|
|
|
if (QDir(cacheLocation).exists()) {
|
|
|
|
QDir(cacheLocation).removeRecursively();
|
|
|
|
}
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
void LauncherCore::refreshNews()
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-11 14:49:24 -04:00
|
|
|
fetchNews();
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2024-03-22 20:12:06 -04:00
|
|
|
void LauncherCore::refreshLogoImage()
|
|
|
|
{
|
|
|
|
const QDir cacheDir = QStandardPaths::standardLocations(QStandardPaths::StandardLocation::CacheLocation).last();
|
2024-03-22 20:29:00 -04:00
|
|
|
const QDir logoDir = cacheDir.absoluteFilePath(QStringLiteral("logos"));
|
2024-03-22 20:12:06 -04:00
|
|
|
|
2024-03-22 20:29:00 -04:00
|
|
|
if (!logoDir.exists()) {
|
2024-07-04 20:53:06 -04:00
|
|
|
Q_UNUSED(QDir().mkpath(logoDir.absolutePath()))
|
2024-03-22 20:12:06 -04:00
|
|
|
}
|
|
|
|
|
2024-03-22 20:29:00 -04:00
|
|
|
const auto saveTexture = [](GameData *data, const QString &path, const QString &name) {
|
|
|
|
if (QFile::exists(name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto file = physis_gamedata_extract_file(data, path.toStdString().c_str());
|
2024-03-22 20:29:00 -04:00
|
|
|
if (file.data != nullptr) {
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto tex = physis_texture_parse(file);
|
2024-03-22 20:29:00 -04:00
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const QImage image(tex.rgba, tex.width, tex.height, QImage::Format_RGBA8888);
|
|
|
|
Q_UNUSED(image.save(name))
|
2024-03-22 20:29:00 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: this finds the first profile that has a valid image, but this could probably be cached per-profile
|
2024-03-22 20:12:06 -04:00
|
|
|
for (int i = 0; i < m_profileManager->numProfiles(); i++) {
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto profile = m_profileManager->getProfile(i);
|
2024-03-22 20:12:06 -04:00
|
|
|
if (profile->isGameInstalled() && profile->gameData()) {
|
2024-03-22 20:29:00 -04:00
|
|
|
// A Realm Reborn
|
|
|
|
saveTexture(profile->gameData(), QStringLiteral("ui/uld/Title_Logo.tex"), logoDir.absoluteFilePath(QStringLiteral("ffxiv.png")));
|
2024-03-22 20:12:06 -04:00
|
|
|
|
2024-03-22 20:29:00 -04:00
|
|
|
for (int j = 0; j < profile->numInstalledExpansions(); j++) {
|
|
|
|
const int expansionNumber = 100 * (j + 3); // logo number starts at 300 for ex1
|
2024-03-22 20:12:06 -04:00
|
|
|
|
2024-03-22 20:29:00 -04:00
|
|
|
saveTexture(profile->gameData(),
|
|
|
|
QStringLiteral("ui/uld/Title_Logo%1_hr1.tex").arg(expansionNumber),
|
|
|
|
logoDir.absoluteFilePath(QStringLiteral("ex%1.png").arg(j + 1)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-03-22 20:12:06 -04:00
|
|
|
|
2024-03-22 20:29:00 -04:00
|
|
|
QList<QString> imageFiles;
|
|
|
|
|
|
|
|
// TODO: sort
|
|
|
|
QDirIterator it(logoDir.absolutePath());
|
|
|
|
while (it.hasNext()) {
|
|
|
|
const QFileInfo logoFile(it.next());
|
|
|
|
if (logoFile.completeSuffix() != QStringLiteral("png")) {
|
|
|
|
continue;
|
2024-03-22 20:12:06 -04:00
|
|
|
}
|
2024-03-22 20:29:00 -04:00
|
|
|
|
|
|
|
imageFiles.push_back(logoFile.absoluteFilePath());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!imageFiles.isEmpty()) {
|
|
|
|
m_cachedLogoImage = imageFiles.last();
|
|
|
|
Q_EMIT cachedLogoImageChanged();
|
2024-03-22 20:12:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
Profile *LauncherCore::currentProfile() const
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-11 14:49:24 -04:00
|
|
|
return m_profileManager->getProfile(m_currentProfileIndex);
|
|
|
|
}
|
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
void LauncherCore::setCurrentProfile(const Profile *profile)
|
2023-10-11 14:49:24 -04:00
|
|
|
{
|
|
|
|
Q_ASSERT(profile != nullptr);
|
|
|
|
|
|
|
|
const int newIndex = m_profileManager->getProfileIndex(profile->uuid());
|
|
|
|
if (newIndex != m_currentProfileIndex) {
|
|
|
|
m_currentProfileIndex = newIndex;
|
2023-12-17 11:23:17 -05:00
|
|
|
m_settings->setCurrentProfile(profile->uuid());
|
2023-10-11 14:49:24 -04:00
|
|
|
m_settings->config()->save();
|
|
|
|
Q_EMIT currentProfileChanged();
|
|
|
|
}
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2023-09-17 19:20:41 -04:00
|
|
|
[[nodiscard]] QString LauncherCore::autoLoginProfileName() const
|
|
|
|
{
|
2023-10-11 13:25:24 -04:00
|
|
|
return m_settings->config()->autoLoginProfile();
|
2023-09-17 19:20:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] Profile *LauncherCore::autoLoginProfile() const
|
|
|
|
{
|
2023-10-11 13:25:24 -04:00
|
|
|
if (m_settings->config()->autoLoginProfile().isEmpty()) {
|
2023-09-17 19:20:41 -04:00
|
|
|
return nullptr;
|
|
|
|
}
|
2023-10-11 13:25:24 -04:00
|
|
|
return m_profileManager->getProfileByUUID(m_settings->config()->autoLoginProfile());
|
2023-09-17 19:20:41 -04:00
|
|
|
}
|
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
void LauncherCore::setAutoLoginProfile(const Profile *profile)
|
2023-09-17 19:20:41 -04:00
|
|
|
{
|
2023-12-17 12:01:28 -05:00
|
|
|
if (profile != nullptr) {
|
|
|
|
auto uuid = profile->uuid();
|
|
|
|
if (uuid != m_settings->config()->autoLoginProfile()) {
|
|
|
|
m_settings->config()->setAutoLoginProfile(uuid);
|
|
|
|
}
|
|
|
|
} else {
|
2023-10-11 13:25:24 -04:00
|
|
|
m_settings->config()->setAutoLoginProfile({});
|
2023-09-17 19:20:41 -04:00
|
|
|
}
|
|
|
|
|
2023-12-17 12:01:28 -05:00
|
|
|
m_settings->config()->save();
|
|
|
|
Q_EMIT autoLoginProfileChanged();
|
2023-09-17 19:20:41 -04:00
|
|
|
}
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
void LauncherCore::buildRequest(const Profile &settings, QNetworkRequest &request)
|
2023-09-16 18:37:42 -04:00
|
|
|
{
|
2023-12-17 13:05:37 -05:00
|
|
|
Utility::setSSL(request);
|
2023-10-11 14:49:24 -04:00
|
|
|
|
|
|
|
if (settings.account()->license() == Account::GameLicense::macOS) {
|
|
|
|
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("macSQEXAuthor/2.0.0(MacOSX; ja-jp)"));
|
|
|
|
} else {
|
2023-12-17 10:09:01 -05:00
|
|
|
request.setHeader(QNetworkRequest::UserAgentHeader,
|
|
|
|
QStringLiteral("SQEXAuthor/2.0.0(Windows 6.2; ja-jp; %1)").arg(QString::fromUtf8(QSysInfo::bootUniqueId())));
|
2023-10-11 14:49:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
request.setRawHeader(QByteArrayLiteral("Accept"),
|
|
|
|
QByteArrayLiteral("image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, "
|
|
|
|
"application/x-ms-xbap, */*"));
|
|
|
|
request.setRawHeader(QByteArrayLiteral("Accept-Encoding"), QByteArrayLiteral("gzip, deflate"));
|
|
|
|
request.setRawHeader(QByteArrayLiteral("Accept-Language"), QByteArrayLiteral("en-us"));
|
|
|
|
}
|
|
|
|
|
|
|
|
void LauncherCore::setupIgnoreSSL(QNetworkReply *reply)
|
|
|
|
{
|
|
|
|
Q_ASSERT(reply != nullptr);
|
|
|
|
|
|
|
|
if (m_settings->preferredProtocol() == QStringLiteral("http")) {
|
|
|
|
connect(reply, &QNetworkReply::sslErrors, this, [reply](const QList<QSslError> &errors) {
|
|
|
|
reply->ignoreSslErrors(errors);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LauncherCore::isLoadingFinished() const
|
|
|
|
{
|
|
|
|
return m_loadingFinished;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LauncherCore::isSteam() const
|
|
|
|
{
|
|
|
|
return m_steamApi != nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LauncherCore::isSteamDeck() const
|
|
|
|
{
|
2024-08-04 22:48:02 -04:00
|
|
|
return Utility::isSteamDeck();
|
2023-10-11 14:49:24 -04:00
|
|
|
}
|
|
|
|
|
2024-05-18 14:26:33 -04:00
|
|
|
bool LauncherCore::isWindows()
|
2024-04-27 16:38:22 +00:00
|
|
|
{
|
|
|
|
#if defined(Q_OS_WIN)
|
|
|
|
return true;
|
|
|
|
#else
|
|
|
|
return false;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2024-05-18 14:26:33 -04:00
|
|
|
bool LauncherCore::needsCompatibilityTool()
|
|
|
|
{
|
|
|
|
return !isWindows();
|
|
|
|
}
|
|
|
|
|
2023-10-11 17:45:02 -04:00
|
|
|
bool LauncherCore::isPatching() const
|
|
|
|
{
|
|
|
|
return m_isPatching;
|
|
|
|
}
|
|
|
|
|
2024-07-30 18:43:06 -04:00
|
|
|
bool LauncherCore::supportsSync() const
|
|
|
|
{
|
|
|
|
#ifdef BUILD_SYNC
|
|
|
|
return true;
|
|
|
|
#else
|
|
|
|
return false;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
QNetworkAccessManager *LauncherCore::mgr()
|
|
|
|
{
|
|
|
|
return m_mgr;
|
|
|
|
}
|
|
|
|
|
|
|
|
LauncherSettings *LauncherCore::settings()
|
|
|
|
{
|
|
|
|
return m_settings;
|
|
|
|
}
|
|
|
|
|
|
|
|
ProfileManager *LauncherCore::profileManager()
|
|
|
|
{
|
|
|
|
return m_profileManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
AccountManager *LauncherCore::accountManager()
|
|
|
|
{
|
|
|
|
return m_accountManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
Headline *LauncherCore::headline() const
|
|
|
|
{
|
|
|
|
return m_headline;
|
|
|
|
}
|
|
|
|
|
2024-03-22 20:12:06 -04:00
|
|
|
QString LauncherCore::cachedLogoImage() const
|
|
|
|
{
|
|
|
|
return m_cachedLogoImage;
|
|
|
|
}
|
|
|
|
|
2024-07-30 18:43:06 -04:00
|
|
|
#ifdef BUILD_SYNC
|
2024-07-28 11:17:30 -04:00
|
|
|
SyncManager *LauncherCore::syncManager() const
|
|
|
|
{
|
|
|
|
return m_syncManager;
|
|
|
|
}
|
2024-07-30 18:43:06 -04:00
|
|
|
#endif
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2023-10-11 14:49:24 -04:00
|
|
|
QCoro::Task<> LauncherCore::beginLogin(LoginInformation &info)
|
|
|
|
{
|
2024-04-19 20:29:28 -04:00
|
|
|
// Hmm, I don't think we're set up for this yet?
|
|
|
|
if (!info.profile->isBenchmark()) {
|
2024-08-22 18:50:43 -04:00
|
|
|
updateConfig(info.profile->account());
|
2024-04-19 20:29:28 -04:00
|
|
|
}
|
2023-10-11 14:49:24 -04:00
|
|
|
|
2024-07-30 18:43:06 -04:00
|
|
|
#ifdef BUILD_SYNC
|
2024-07-28 11:17:30 -04:00
|
|
|
const auto characterSync = new CharacterSync(*info.profile->account(), *this, this);
|
|
|
|
if (!co_await characterSync->sync()) {
|
|
|
|
co_return;
|
|
|
|
}
|
2024-07-30 18:43:06 -04:00
|
|
|
#endif
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-06-27 16:38:45 -04:00
|
|
|
std::optional<LoginAuth> auth;
|
|
|
|
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);
|
2023-10-11 14:49:24 -04:00
|
|
|
}
|
2024-06-27 16:38:45 -04:00
|
|
|
}
|
2023-10-11 14:49:24 -04:00
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto assetUpdater = new AssetUpdater(*info.profile, *this, this);
|
2024-06-27 16:38:45 -04:00
|
|
|
if (co_await assetUpdater->update()) {
|
2024-04-19 20:29:28 -04:00
|
|
|
// If we expect an auth ticket, don't continue if missing
|
|
|
|
if (!info.profile->isBenchmark() && auth == std::nullopt) {
|
|
|
|
co_return;
|
|
|
|
}
|
2023-10-11 14:49:24 -04:00
|
|
|
|
2024-04-19 20:29:28 -04:00
|
|
|
Q_EMIT stageChanged(i18n("Launching game..."));
|
2023-10-11 14:49:24 -04:00
|
|
|
|
2024-04-19 20:29:28 -04:00
|
|
|
if (isSteam()) {
|
|
|
|
m_steamApi->setLauncherMode(false);
|
2023-10-11 14:49:24 -04:00
|
|
|
}
|
2024-04-19 20:29:28 -04:00
|
|
|
|
|
|
|
m_runner->beginGameExecutable(*info.profile, auth);
|
2023-10-11 14:49:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
assetUpdater->deleteLater();
|
2023-09-16 18:37:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
QCoro::Task<> LauncherCore::fetchNews()
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-08 18:02:02 -04:00
|
|
|
qInfo(ASTRA_LOG) << "Fetching news...";
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
QUrlQuery query;
|
2023-09-17 08:31:24 -04:00
|
|
|
query.addQueryItem(QStringLiteral("lang"), QStringLiteral("en-us"));
|
|
|
|
query.addQueryItem(QStringLiteral("media"), QStringLiteral("pcapp"));
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-06-29 19:57:16 -04:00
|
|
|
QUrl headlineUrl;
|
|
|
|
headlineUrl.setScheme(m_settings->preferredProtocol());
|
|
|
|
headlineUrl.setHost(QStringLiteral("frontier.%1").arg(m_settings->squareEnixServer()));
|
|
|
|
headlineUrl.setPath(QStringLiteral("/news/headline.json"));
|
|
|
|
headlineUrl.setQuery(query);
|
|
|
|
|
|
|
|
QNetworkRequest headlineRequest(QUrl(QStringLiteral("%1&%2").arg(headlineUrl.toString(), QString::number(QDateTime::currentMSecsSinceEpoch()))));
|
|
|
|
headlineRequest.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("application/json, text/plain, */*"));
|
|
|
|
headlineRequest.setRawHeader(QByteArrayLiteral("Origin"), QByteArrayLiteral("https://launcher.finalfantasyxiv.com"));
|
2024-06-29 20:08:17 -04:00
|
|
|
headlineRequest.setRawHeader(
|
|
|
|
QByteArrayLiteral("Referer"),
|
|
|
|
QStringLiteral("%1/index.html?rc_lang=%2&time=%3")
|
|
|
|
.arg(currentProfile()->frontierUrl(), QStringLiteral("en-us"), QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyy-MM-dd-HH")))
|
|
|
|
.toUtf8());
|
2024-06-29 19:57:16 -04:00
|
|
|
Utility::printRequest(QStringLiteral("GET"), headlineRequest);
|
|
|
|
|
|
|
|
auto headlineReply = mgr()->get(headlineRequest);
|
|
|
|
co_await headlineReply;
|
|
|
|
|
|
|
|
QUrl bannerUrl;
|
|
|
|
bannerUrl.setScheme(m_settings->preferredProtocol());
|
|
|
|
bannerUrl.setHost(QStringLiteral("frontier.%1").arg(m_settings->squareEnixServer()));
|
|
|
|
bannerUrl.setPath(QStringLiteral("/v2/topics/%1/banner.json").arg(QStringLiteral("en-us")));
|
|
|
|
bannerUrl.setQuery(query);
|
|
|
|
|
|
|
|
QNetworkRequest bannerRequest(QUrl(QStringLiteral("%1&_=%3").arg(bannerUrl.toString(), QString::number(QDateTime::currentMSecsSinceEpoch()))));
|
|
|
|
bannerRequest.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("application/json, text/plain, */*"));
|
|
|
|
bannerRequest.setRawHeader(QByteArrayLiteral("Origin"), QByteArrayLiteral("https://launcher.finalfantasyxiv.com"));
|
2024-06-29 20:08:17 -04:00
|
|
|
bannerRequest.setRawHeader(
|
|
|
|
QByteArrayLiteral("Referer"),
|
|
|
|
QStringLiteral("%1/v700/index.html?rc_lang=%2&time=%3")
|
|
|
|
.arg(currentProfile()->frontierUrl(), QStringLiteral("en-us"), QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyy-MM-dd-HH")))
|
|
|
|
.toUtf8());
|
2024-06-29 19:57:16 -04:00
|
|
|
Utility::printRequest(QStringLiteral("GET"), bannerRequest);
|
|
|
|
|
|
|
|
auto bannerReply = mgr()->get(bannerRequest);
|
|
|
|
co_await bannerReply;
|
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto document = QJsonDocument::fromJson(headlineReply->readAll());
|
|
|
|
const auto bannerDocument = QJsonDocument::fromJson(bannerReply->readAll());
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const auto headline = new Headline(this);
|
2024-06-29 19:57:16 -04:00
|
|
|
if (document.isEmpty() || bannerDocument.isEmpty()) {
|
2023-09-16 18:37:42 -04:00
|
|
|
headline->failedToLoad = true;
|
2024-06-29 19:57:16 -04:00
|
|
|
} else {
|
|
|
|
const auto parseNews = [](QJsonObject object) -> News {
|
|
|
|
News news;
|
|
|
|
news.date = QDateTime::fromString(object["date"_L1].toString(), Qt::DateFormat::ISODate);
|
|
|
|
news.id = object["id"_L1].toString();
|
|
|
|
news.tag = object["tag"_L1].toString();
|
|
|
|
news.title = object["title"_L1].toString();
|
|
|
|
|
|
|
|
if (object["url"_L1].toString().isEmpty()) {
|
|
|
|
news.url = QUrl(QStringLiteral("https://na.finalfantasyxiv.com/lodestone/news/detail/%1").arg(news.id));
|
|
|
|
} else {
|
|
|
|
news.url = QUrl(object["url"_L1].toString());
|
|
|
|
}
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-06-29 19:57:16 -04:00
|
|
|
return news;
|
|
|
|
};
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-06-29 19:57:16 -04:00
|
|
|
for (const auto bannerObject : bannerDocument.object()["banner"_L1].toArray()) {
|
|
|
|
// TODO: use new order_priority and fix_order params
|
|
|
|
headline->banners.push_back(
|
|
|
|
{.link = QUrl(bannerObject.toObject()["link"_L1].toString()), .bannerImage = QUrl(bannerObject.toObject()["lsb_banner"_L1].toString())});
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
|
|
|
|
2024-06-29 19:57:16 -04:00
|
|
|
for (const auto newsObject : document.object()["news"_L1].toArray()) {
|
|
|
|
headline->news.push_back(parseNews(newsObject.toObject()));
|
|
|
|
}
|
2023-09-16 18:37:42 -04:00
|
|
|
|
2024-06-29 19:57:16 -04:00
|
|
|
for (const auto pinnedObject : document.object()["pinned"_L1].toArray()) {
|
|
|
|
headline->pinned.push_back(parseNews(pinnedObject.toObject()));
|
|
|
|
}
|
2023-09-16 18:37:42 -04:00
|
|
|
|
2024-06-29 19:57:16 -04:00
|
|
|
for (const auto pinnedObject : document.object()["topics"_L1].toArray()) {
|
|
|
|
headline->topics.push_back(parseNews(pinnedObject.toObject()));
|
|
|
|
}
|
2023-09-16 18:37:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
m_headline = headline;
|
|
|
|
Q_EMIT newsChanged();
|
2023-07-30 08:49:34 -04:00
|
|
|
}
|
2023-12-17 11:12:13 -05:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
QCoro::Task<> LauncherCore::handleGameExit(const Profile *profile)
|
2024-07-30 19:47:26 -04:00
|
|
|
{
|
2024-08-22 21:22:56 -04:00
|
|
|
qCDebug(ASTRA_LOG) << "Game has closed.";
|
|
|
|
|
|
|
|
uninhibitSleep();
|
|
|
|
|
2024-07-30 19:47:26 -04:00
|
|
|
#ifdef BUILD_SYNC
|
2024-07-30 20:41:35 -04:00
|
|
|
// TODO: once we have Steam API support we can tell Steam to delay putting the Deck to sleep until our upload is complete
|
2024-07-30 19:58:40 -04:00
|
|
|
if (m_settings->enableSync()) {
|
|
|
|
Q_EMIT showWindow();
|
|
|
|
|
|
|
|
qCDebug(ASTRA_LOG) << "Game closed! Uploading character data...";
|
|
|
|
const auto characterSync = new CharacterSync(*profile->account(), *this, this);
|
|
|
|
co_await characterSync->sync(false);
|
2024-07-30 19:47:26 -04:00
|
|
|
|
2024-07-30 19:58:40 -04:00
|
|
|
// Tell the user they can now quit.
|
|
|
|
Q_EMIT stageChanged(i18n("You may now safely close the game."));
|
|
|
|
|
|
|
|
co_return;
|
|
|
|
}
|
|
|
|
#endif
|
2024-07-30 19:47:26 -04:00
|
|
|
// Otherwise, quit when everything is finished.
|
|
|
|
if (m_settings->closeWhenLaunched()) {
|
|
|
|
QCoreApplication::exit();
|
|
|
|
}
|
2024-07-30 19:58:40 -04:00
|
|
|
|
2024-07-30 19:47:26 -04:00
|
|
|
co_return;
|
|
|
|
}
|
|
|
|
|
2024-08-22 18:50:43 -04:00
|
|
|
void LauncherCore::updateConfig(const Account *account)
|
|
|
|
{
|
|
|
|
const auto configDir = account->getConfigDir().absoluteFilePath(QStringLiteral("FFXIV.cfg"));
|
|
|
|
|
|
|
|
if (!QFile::exists(configDir)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
qInfo(ASTRA_LOG) << "Updating FFXIV.cfg...";
|
|
|
|
|
|
|
|
const auto configDirStd = configDir.toStdString();
|
|
|
|
|
|
|
|
const auto cfgFileBuffer = physis_read_file(configDirStd.c_str());
|
|
|
|
const auto cfgFile = physis_cfg_parse(cfgFileBuffer);
|
|
|
|
|
|
|
|
// Ensure that the opening cutscene movie never plays, since it's broken in most versions of Wine
|
|
|
|
physis_cfg_set_value(cfgFile, "CutsceneMovieOpening", "1");
|
|
|
|
|
|
|
|
const auto screenshotDir = settings()->screenshotDir();
|
|
|
|
Utility::createPathIfNeeded(screenshotDir);
|
|
|
|
|
|
|
|
const auto screenshotDirWin = Utility::toWindowsPath(screenshotDir);
|
|
|
|
const auto screenshotDirWinStd = screenshotDirWin.toStdString();
|
|
|
|
|
|
|
|
// Set the screenshot path
|
|
|
|
physis_cfg_set_value(cfgFile, "ScreenShotDir", screenshotDirWinStd.c_str());
|
|
|
|
|
|
|
|
const auto buffer = physis_cfg_write(cfgFile);
|
|
|
|
|
|
|
|
QFile file(configDir);
|
|
|
|
file.open(QIODevice::WriteOnly);
|
|
|
|
file.write(reinterpret_cast<const char *>(buffer.data), buffer.size);
|
|
|
|
file.close();
|
|
|
|
}
|
|
|
|
|
2024-08-22 21:22:56 -04:00
|
|
|
void LauncherCore::inhibitSleep()
|
|
|
|
{
|
|
|
|
#ifdef HAS_DBUS
|
|
|
|
if (screenSaverDbusCookie != 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
|
|
|
|
QStringLiteral("/ScreenSaver"),
|
|
|
|
QStringLiteral("org.freedesktop.ScreenSaver"),
|
|
|
|
QStringLiteral("Inhibit"));
|
|
|
|
message << QGuiApplication::desktopFileName();
|
|
|
|
message << i18n("Playing FFXIV");
|
|
|
|
|
|
|
|
const QDBusReply<uint> reply = QDBusConnection::sessionBus().call(message);
|
|
|
|
if (reply.isValid()) {
|
|
|
|
screenSaverDbusCookie = reply.value();
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
void LauncherCore::uninhibitSleep()
|
|
|
|
{
|
|
|
|
#ifdef HAS_DBUS
|
|
|
|
if (screenSaverDbusCookie == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
|
|
|
|
QStringLiteral("/ScreenSaver"),
|
|
|
|
QStringLiteral("org.freedesktop.ScreenSaver"),
|
|
|
|
QStringLiteral("UnInhibit"));
|
|
|
|
message << static_cast<uint>(screenSaverDbusCookie);
|
|
|
|
screenSaverDbusCookie = 0;
|
|
|
|
QDBusConnection::sessionBus().send(message);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-12-17 11:12:13 -05:00
|
|
|
#include "moc_launchercore.cpp"
|