// SPDX-FileCopyrightText: 2023 Joshua Goins // SPDX-License-Identifier: GPL-3.0-or-later #include "patcher.h" #include #include #include #include #include #include #include #include #include #include "astra_patcher_log.h" #include "launchercore.h" #include "patchlist.h" #include "utility.h" using namespace Qt::StringLiterals; Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, BootData &bootData, QObject *parent) : QObject(parent) , m_baseDirectory(baseDirectory) , m_bootData(&bootData) , m_launcher(launcher) { m_launcher.m_isPatching = true; setupDirectories(); Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version", getBaseString())); } Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, GameData &gameData, QObject *parent) : QObject(parent) , m_baseDirectory(baseDirectory) , m_gameData(&gameData) , m_launcher(launcher) { m_launcher.m_isPatching = true; setupDirectories(); Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version", getBaseString())); } Patcher::~Patcher() { m_launcher.m_isPatching = false; } QCoro::Task Patcher::patch(const PatchList &patchList) { if (patchList.isEmpty()) { co_return false; } Q_EMIT m_launcher.stageIndeterminate(); Q_EMIT m_launcher.stageChanged(i18n("Updating %1", getBaseString())); m_remainingPatches = static_cast(patchList.patches().size()); m_patchQueue.resize(m_remainingPatches); QFutureSynchronizer synchronizer; int patchIndex = 0; for (auto &patch : patchList.patches()) { const int ourIndex = patchIndex++; const QString filename = QStringLiteral("%1.patch").arg(patch.name); const QDir repositoryDir = m_patchesDir.absoluteFilePath(patch.repository); Utility::createPathIfNeeded(repositoryDir); const QString patchPath = repositoryDir.absoluteFilePath(filename); const QueuedPatch queuedPatch{.name = patch.name, .repository = patch.repository, .version = patch.version, .path = patchPath, .hashes = patch.hashes, .hashBlockSize = patch.hashBlockSize, .length = patch.length, .isBoot = isBoot()}; qDebug(ASTRA_PATCHER) << "Adding a queued patch:"; qDebug(ASTRA_PATCHER) << "- Name:" << patch.name; qDebug(ASTRA_PATCHER) << "- Repository or is boot:" << (isBoot() ? QStringLiteral("boot") : patch.repository); qDebug(ASTRA_PATCHER) << "- Version:" << patch.version; qDebug(ASTRA_PATCHER) << "- Downloaded Path:" << patchPath; qDebug(ASTRA_PATCHER) << "- Hashes:" << patch.hashes; qDebug(ASTRA_PATCHER) << "- Hash Block Size:" << patch.hashBlockSize; qDebug(ASTRA_PATCHER) << "- Length:" << patch.length; m_patchQueue[ourIndex] = queuedPatch; if (!QFile::exists(patchPath)) { const auto patchRequest = QNetworkRequest(QUrl(patch.url)); Utility::printRequest(QStringLiteral("GET"), patchRequest); auto patchReply = m_launcher.mgr()->get(patchRequest); connect(patchReply, &QNetworkReply::downloadProgress, this, [this, ourIndex, queuedPatch](int received, int total) { Q_UNUSED(total) updateDownloadProgress(ourIndex, received); }); synchronizer.addFuture(QtFuture::connect(patchReply, &QNetworkReply::finished).then([this, ourIndex, patchPath, patchReply] { qDebug(ASTRA_PATCHER) << "Downloaded to" << patchPath; QFile file(patchPath); file.open(QIODevice::WriteOnly); file.write(patchReply->readAll()); file.close(); QMutexLocker locker(&m_finishedPatchesMutex); m_finishedPatches++; m_patchQueue[ourIndex].downloaded = true; updateMessage(); })); } else { m_patchQueue[ourIndex].downloaded = true; m_finishedPatches++; qDebug(ASTRA_PATCHER) << "Found existing patch: " << patch.name; } } co_await QtConcurrent::run([&synchronizer] { synchronizer.waitForFinished(); }); // This must happen synchronously int i = 0; for (const auto &patch : m_patchQueue) { QString repositoryName = patch.repository; if (repositoryName == QStringLiteral("game")) { repositoryName = QStringLiteral("ffxiv"); } Q_EMIT m_launcher.stageChanged(i18n("Installing %1 - %2 [%3/%4]", repositoryName, patch.version, i++, m_remainingPatches)); Q_EMIT m_launcher.stageDeterminate(0, static_cast(m_patchQueue.size()), i++); co_await QtConcurrent::run([this, patch] { processPatch(patch); }); } co_return true; } void Patcher::processPatch(const QueuedPatch &patch) { // Perform hash checking if (!patch.hashes.isEmpty()) { auto f = QFile(patch.path); f.open(QIODevice::ReadOnly); Q_ASSERT(patch.length == f.size()); const int parts = std::ceil(static_cast(patch.length) / static_cast(patch.hashBlockSize)); QByteArray block; block.resize(patch.hashBlockSize); for (int i = 0; i < parts; i++) { const auto read = f.read(patch.hashBlockSize); if (read.length() <= patch.hashBlockSize) { block = read; } QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(block); Q_ASSERT(QString::fromUtf8(hash.result().toHex()) == patch.hashes[i]); } } if (isBoot()) { physis_bootdata_apply_patch(m_bootData, patch.path.toStdString().c_str()); } else { physis_gamedata_apply_patch(m_gameData, patch.path.toStdString().c_str()); } qDebug(ASTRA_PATCHER) << "Installed" << patch.path << "to" << (isBoot() ? QStringLiteral("boot") : patch.repository); QString verFilePath; if (isBoot()) { verFilePath = m_baseDirectory + QStringLiteral("/ffxivboot.ver"); } else { if (patch.repository == "game"_L1) { verFilePath = m_baseDirectory + QStringLiteral("/ffxivgame.ver"); } else { verFilePath = m_baseDirectory + QStringLiteral("/sqpack/") + patch.repository + QStringLiteral("/") + patch.repository + QStringLiteral(".ver"); } } QFile verFile(verFilePath); verFile.open(QIODevice::WriteOnly | QIODevice::Text); verFile.write(patch.version.toUtf8()); verFile.close(); } void Patcher::setupDirectories() { QDir dataDir; if (m_launcher.settings()->keepPatches()) { dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); } else { dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); } m_patchesDir.setPath(dataDir.absoluteFilePath(QStringLiteral("patches"))); } QString Patcher::getBaseString() const { if (isBoot()) { return i18n("FINAL FANTASY XIV Update/Launcher"); } else { return i18n("FINAL FANTASY XIV Game"); } } void Patcher::updateDownloadProgress(const int index, int received) { QMutexLocker locker(&m_finishedPatchesMutex); m_patchQueue[index].bytesDownloaded = received; updateMessage(); } void Patcher::updateMessage() { // Find first not-downloaded patch for (const auto &patch : m_patchQueue) { if (!patch.downloaded) { QString repositoryName = patch.repository; if (repositoryName == QStringLiteral("game")) { repositoryName = QStringLiteral("ffxiv"); } const float progress = ((float)patch.bytesDownloaded / (float)patch.length) * 100.0f; const QString progressStr = QStringLiteral("%1").arg(progress, 1, 'f', 1, QLatin1Char('0')); Q_EMIT m_launcher.stageChanged(i18n("Downloading %1 - %2 [%3/%4]", repositoryName, patch.version, m_finishedPatches, m_remainingPatches), i18n("%1%", progressStr)); Q_EMIT m_launcher.stageDeterminate(0, static_cast(patch.length), static_cast(patch.bytesDownloaded)); return; } } } #include "moc_patcher.cpp"