From a0da4d02b7ed272ab22bec38b4d9c268507dd812 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 30 Jul 2024 19:47:26 -0400 Subject: [PATCH] Implement sync locking, and upload data when quitting the game --- launcher/include/charactersync.h | 3 ++- launcher/include/launchercore.h | 4 ++- launcher/include/syncmanager.h | 9 +++++++ launcher/src/charactersync.cpp | 33 +++++++++++++++++------- launcher/src/gamerunner.cpp | 6 ++--- launcher/src/launchercore.cpp | 17 ++++++++++++ launcher/src/syncmanager.cpp | 32 +++++++++++++++++++++++ launcher/ui/Main.qml | 4 +-- launcher/ui/Settings/GeneralSettings.qml | 2 +- 9 files changed, 92 insertions(+), 18 deletions(-) diff --git a/launcher/include/charactersync.h b/launcher/include/charactersync.h index b7a2148..c40dae9 100644 --- a/launcher/include/charactersync.h +++ b/launcher/include/charactersync.h @@ -21,8 +21,9 @@ public: explicit CharacterSync(Account &account, LauncherCore &launcher, QObject *parent = nullptr); /// Checks and synchronizes character files as necessary. + /// \param initialSync Whether this is the initial sync on game start. /// \return False if the synchronization failed. - QCoro::Task sync(); + QCoro::Task sync(bool initialSync = true); private: QCoro::Task uploadCharacterData(const QDir &dir, const QString &id); diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h index 587e1b0..2cb6fcf 100755 --- a/launcher/include/launchercore.h +++ b/launcher/include/launchercore.h @@ -136,7 +136,7 @@ public: Q_SIGNALS: void loadingFinished(); void successfulLaunch(); - void gameClosed(); + void gameClosed(Profile *profile); void loginError(QString message); void dalamudError(QString message); void miscError(QString message); @@ -158,6 +158,8 @@ private: QCoro::Task<> fetchNews(); + QCoro::Task<> handleGameExit(Profile *profile); + SteamAPI *m_steamApi = nullptr; bool m_loadingFinished = false; diff --git a/launcher/include/syncmanager.h b/launcher/include/syncmanager.h index 24096ea..a58e813 100644 --- a/launcher/include/syncmanager.h +++ b/launcher/include/syncmanager.h @@ -61,6 +61,15 @@ public: /// Downloads character data QCoro::Task downloadCharacterData(const QString &mxcUri, const QString &destPath); + /// Checks the lock on the sync + QCoro::Task> checkLock(); + + /// Sets the sync lock to the device's hostname + QCoro::Task<> setLock(); + + /// Breaks the sync lock + QCoro::Task<> breakLock(); + Q_SIGNALS: void connectedChanged(); void userIdChanged(); diff --git a/launcher/src/charactersync.cpp b/launcher/src/charactersync.cpp index 39ac838..8813848 100644 --- a/launcher/src/charactersync.cpp +++ b/launcher/src/charactersync.cpp @@ -16,14 +16,12 @@ CharacterSync::CharacterSync(Account &account, LauncherCore &launcher, QObject * { } -QCoro::Task CharacterSync::sync() +QCoro::Task CharacterSync::sync(const bool initialSync) { if (!launcher.settings()->enableSync()) { co_return true; } - qInfo() << "A"; - auto syncManager = launcher.syncManager(); if (!syncManager->connected()) { qInfo() << "B"; @@ -32,8 +30,6 @@ QCoro::Task CharacterSync::sync() co_return false; } - qInfo() << "C"; - if (!syncManager->isReady()) { Q_EMIT launcher.stageChanged(i18n("Waiting for sync connection...")); @@ -43,6 +39,21 @@ QCoro::Task CharacterSync::sync() Q_EMIT launcher.stageChanged(i18n("Synchronizing character data...")); + // On game boot, check if we need the lock. Otherwise break it when we clean up. + if (initialSync) { + if (const auto hostname = co_await syncManager->checkLock(); hostname.has_value()) { + // Don't warn about our own failures + if (hostname != QSysInfo::machineHostName()) { + Q_EMIT launcher.loginError(i18n("Device %1 has not yet uploaded it's character data. Astra will not continue until that device is re-synced.")); + co_return false; + } + } + + syncManager->setLock(); + } else { + syncManager->breakLock(); + } + // so first, we need to list the character folders // we sync each one separately QList characterDirs; @@ -63,15 +74,19 @@ QCoro::Task CharacterSync::sync() for (const auto &dir : characterDirs) { const QString id = dir.fileName(); // FFXIV_CHR0040000001000001 for example const auto previousData = co_await syncManager->getUploadedCharacterData(id); - if (!previousData.has_value()) { + + // TODO: make this a little bit smarter. We shouldn't waste time re-uploading data that's exactly the same. + if (!initialSync || !previousData.has_value()) { // if we didn't upload character data yet, upload it now co_await uploadCharacterData(dir.absoluteFilePath(), id); } else { // otherwise, download it - // but check first if it's our hostname - if (QSysInfo::machineHostName() == previousData->hostname) { - qCDebug(ASTRA_LOG) << "Skipping! We uploaded this data."; + const bool exists = QFile::exists(dir.absoluteFilePath() + QStringLiteral("/GEARSET.DAT")); + + // but check first if it's our hostname. only skip if it exists + if (exists && QSysInfo::machineHostName() == previousData->hostname) { + qCDebug(ASTRA_LOG) << "Skipping" << id << "We uploaded this data."; continue; } diff --git a/launcher/src/gamerunner.cpp b/launcher/src/gamerunner.cpp index 942e41f..e32cdfc 100644 --- a/launcher/src/gamerunner.cpp +++ b/launcher/src/gamerunner.cpp @@ -49,7 +49,7 @@ void GameRunner::beginVanillaGame(const QString &gameExecutablePath, Profile &pr connect(gameProcess, &QProcess::finished, this, [this, &profile](const int exitCode) { profile.setLoggedIn(false); Q_UNUSED(exitCode) - Q_EMIT m_launcher.gameClosed(); + Q_EMIT m_launcher.gameClosed(&profile); }); auto args = getGameArgs(profile, auth); @@ -109,7 +109,7 @@ void GameRunner::beginDalamudGame(const QString &gameExecutablePath, Profile &pr auto watcher = new ProcessWatcher(PID); connect(watcher, &ProcessWatcher::finished, this, [this, &profile] { profile.setLoggedIn(false); - Q_EMIT m_launcher.gameClosed(); + Q_EMIT m_launcher.gameClosed(&profile); }); return; } @@ -117,7 +117,7 @@ void GameRunner::beginDalamudGame(const QString &gameExecutablePath, Profile &pr // If Dalamud didn't give a valid PID, OK. Let's just do our previous status quo and inidcate we did log out. profile.setLoggedIn(false); - Q_EMIT m_launcher.gameClosed(); + Q_EMIT m_launcher.gameClosed(&profile); }); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); diff --git a/launcher/src/launchercore.cpp b/launcher/src/launchercore.cpp index b8cf6cd..2a51fbb 100755 --- a/launcher/src/launchercore.cpp +++ b/launcher/src/launchercore.cpp @@ -40,6 +40,8 @@ LauncherCore::LauncherCore() m_accountManager = new AccountManager(*this, this); m_runner = new GameRunner(*this, this); + connect(this, &LauncherCore::gameClosed, this, &LauncherCore::handleGameExit); + #ifdef BUILD_SYNC m_syncManager = new SyncManager(this); #endif @@ -513,4 +515,19 @@ QCoro::Task<> LauncherCore::fetchNews() Q_EMIT newsChanged(); } +QCoro::Task<> LauncherCore::handleGameExit(Profile *profile) +{ +#ifdef BUILD_SYNC + qCDebug(ASTRA_LOG) << "Game closed! Uploading character data..."; + const auto characterSync = new CharacterSync(*profile->account(), *this, this); + co_await characterSync->sync(false); // TODO: handle errors and especially interactive ones +#endif + + // Otherwise, quit when everything is finished. + if (m_settings->closeWhenLaunched()) { + QCoreApplication::exit(); + } + co_return; +} + #include "moc_launchercore.cpp" \ No newline at end of file diff --git a/launcher/src/syncmanager.cpp b/launcher/src/syncmanager.cpp index 30c6e7b..a3ea977 100644 --- a/launcher/src/syncmanager.cpp +++ b/launcher/src/syncmanager.cpp @@ -21,6 +21,7 @@ const QString roomType = QStringLiteral("zone.xiv.astra-sync"); const QString syncEventType = QStringLiteral("zone.xiv.astra.sync"); +const QString lockEventType = QStringLiteral("zone.xiv.astra.lock"); using namespace Quotient; @@ -250,4 +251,35 @@ QCoro::Task SyncManager::downloadCharacterData(const QString &mxcUri, cons co_return true; } +QCoro::Task> SyncManager::checkLock() +{ + const auto lockEvent = m_currentRoom->currentState().contentJson(syncEventType, QStringLiteral("latest")); + if (lockEvent.isEmpty()) { + co_return std::nullopt; + } + + qCDebug(ASTRA_LOG) << "previous lock event:" << lockEvent; + const QString hostname = lockEvent[QStringLiteral("hostname")].toString(); + if (hostname == QStringLiteral("none")) { + co_return std::nullopt; + } + + co_return hostname; +} + +QCoro::Task<> SyncManager::setLock() +{ + auto lockSetState = + m_currentRoom->setState(syncEventType, QStringLiteral("latest"), QJsonObject{{QStringLiteral("hostname"), QSysInfo::machineHostName()}}); + co_await qCoro(lockSetState, &BaseJob::finished); + co_return; +} + +QCoro::Task<> SyncManager::breakLock() +{ + auto lockSetState = m_currentRoom->setState(syncEventType, QStringLiteral("latest"), QJsonObject{{QStringLiteral("hostname"), QStringLiteral("none")}}); + co_await qCoro(lockSetState, &BaseJob::finished); + co_return; +} + #include "moc_syncmanager.cpp" diff --git a/launcher/ui/Main.qml b/launcher/ui/Main.qml index 589968a..ce0bc67 100644 --- a/launcher/ui/Main.qml +++ b/launcher/ui/Main.qml @@ -113,9 +113,7 @@ Kirigami.ApplicationWindow { } function onGameClosed() { - if (LauncherCore.settings.closeWhenLaunched) { - Qt.callLater(Qt.quit); - } else { + if (!LauncherCore.settings.closeWhenLaunched) { appWindow.checkSetup(); } } diff --git a/launcher/ui/Settings/GeneralSettings.qml b/launcher/ui/Settings/GeneralSettings.qml index 27ce1aa..f831f57 100644 --- a/launcher/ui/Settings/GeneralSettings.qml +++ b/launcher/ui/Settings/GeneralSettings.qml @@ -24,7 +24,7 @@ FormCard.FormCardPage { FormCard.FormCheckDelegate { id: closeAstraDelegate - text: i18n("Close Astra when game is launched") + text: i18n("Hide Astra when game is launched") checked: LauncherCore.settings.closeWhenLaunched onCheckedChanged: LauncherCore.settings.closeWhenLaunched = checked }