2023-08-05 22:14:05 -04:00
|
|
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2022-07-20 18:00:42 -04:00
|
|
|
#include "patcher.h"
|
2023-07-30 08:49:34 -04:00
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
#include <KFormat>
|
2023-07-30 14:56:24 -04:00
|
|
|
#include <KLocalizedString>
|
2022-08-15 11:14:37 -04:00
|
|
|
#include <QDir>
|
2022-07-20 18:00:42 -04:00
|
|
|
#include <QFile>
|
|
|
|
#include <QNetworkRequest>
|
2023-09-16 20:12:42 -04:00
|
|
|
#include <QtConcurrent>
|
2022-07-20 18:00:42 -04:00
|
|
|
#include <physis.hpp>
|
2023-09-16 20:12:42 -04:00
|
|
|
#include <qcorofuture.h>
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2023-10-08 18:02:02 -04:00
|
|
|
#include "astra_patcher_log.h"
|
2023-07-30 14:56:24 -04:00
|
|
|
#include "launchercore.h"
|
2023-10-08 18:02:02 -04:00
|
|
|
#include "utility.h"
|
2023-07-30 14:56:24 -04:00
|
|
|
|
2023-12-17 12:01:28 -05:00
|
|
|
using namespace Qt::StringLiterals;
|
|
|
|
|
2023-09-16 21:18:50 -04:00
|
|
|
Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, BootData &bootData, QObject *parent)
|
2023-07-30 08:49:34 -04:00
|
|
|
: QObject(parent)
|
2023-09-16 21:18:50 -04:00
|
|
|
, m_baseDirectory(baseDirectory)
|
|
|
|
, m_bootData(&bootData)
|
2023-07-30 14:56:24 -04:00
|
|
|
, m_launcher(launcher)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-11 17:45:02 -04:00
|
|
|
m_launcher.m_isPatching = true;
|
|
|
|
|
2023-08-18 14:52:06 -04:00
|
|
|
setupDirectories();
|
|
|
|
|
2023-10-11 17:45:02 -04:00
|
|
|
Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version", getBaseString()));
|
2022-08-09 22:44:10 -04:00
|
|
|
}
|
|
|
|
|
2023-09-16 21:18:50 -04:00
|
|
|
Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, GameData &gameData, QObject *parent)
|
2023-07-30 08:49:34 -04:00
|
|
|
: QObject(parent)
|
2023-09-16 21:18:50 -04:00
|
|
|
, m_baseDirectory(baseDirectory)
|
|
|
|
, m_gameData(&gameData)
|
2023-07-30 14:56:24 -04:00
|
|
|
, m_launcher(launcher)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2023-10-11 17:45:02 -04:00
|
|
|
m_launcher.m_isPatching = true;
|
|
|
|
|
2023-08-18 14:52:06 -04:00
|
|
|
setupDirectories();
|
|
|
|
|
2023-10-11 17:45:02 -04:00
|
|
|
Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version", getBaseString()));
|
|
|
|
}
|
|
|
|
|
|
|
|
Patcher::~Patcher()
|
|
|
|
{
|
|
|
|
m_launcher.m_isPatching = false;
|
2022-07-20 18:00:42 -04:00
|
|
|
}
|
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
QCoro::Task<bool> Patcher::patch(const physis_PatchList &patchList)
|
2023-07-30 08:49:34 -04:00
|
|
|
{
|
2024-08-22 20:42:35 -04:00
|
|
|
if (patchList.num_entries == 0) {
|
|
|
|
co_return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const qint64 neededSpace = patchList.patch_length - m_patchesDirStorageInfo.bytesAvailable();
|
|
|
|
if (neededSpace > 0) {
|
|
|
|
KFormat format;
|
|
|
|
QString neededSpaceStr = format.formatByteSize(neededSpace);
|
|
|
|
Q_EMIT m_launcher.miscError(i18n("There is not enough space available on disk to update the game. You need %1 of free space.", neededSpaceStr));
|
2023-09-17 18:43:58 -04:00
|
|
|
co_return false;
|
2023-09-16 20:12:42 -04:00
|
|
|
}
|
|
|
|
|
2023-09-16 20:30:34 -04:00
|
|
|
Q_EMIT m_launcher.stageIndeterminate();
|
2023-10-11 17:45:02 -04:00
|
|
|
Q_EMIT m_launcher.stageChanged(i18n("Updating %1", getBaseString()));
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
m_remainingPatches = patchList.num_entries;
|
2023-09-16 21:18:50 -04:00
|
|
|
m_patchQueue.resize(m_remainingPatches);
|
2023-07-30 14:56:24 -04:00
|
|
|
|
2023-09-16 20:12:42 -04:00
|
|
|
QFutureSynchronizer<void> synchronizer;
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2023-09-16 20:12:42 -04:00
|
|
|
int patchIndex = 0;
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
for (int i = 0; i < patchList.num_entries; i++) {
|
|
|
|
const auto &patch = patchList.entries[i];
|
|
|
|
|
2023-09-16 20:30:34 -04:00
|
|
|
const int ourIndex = patchIndex++;
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
const QString filename = QStringLiteral("%1.patch").arg(QLatin1String(patch.version));
|
|
|
|
const QString tempFilename = QStringLiteral("%1.patch~").arg(QLatin1String(patch.version)); // tilde afterwards to hide it easily
|
2024-06-26 23:43:06 -04:00
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
const QString repository = Utility::repositoryFromPatchUrl(QLatin1String(patch.url));
|
|
|
|
const QDir repositoryDir = m_patchesDir.absoluteFilePath(repository);
|
2023-12-17 12:01:28 -05:00
|
|
|
Utility::createPathIfNeeded(repositoryDir);
|
2022-11-17 12:22:56 -05:00
|
|
|
|
2023-09-16 20:12:42 -04:00
|
|
|
const QString patchPath = repositoryDir.absoluteFilePath(filename);
|
2024-06-26 23:43:06 -04:00
|
|
|
const QString tempPatchPath = repositoryDir.absoluteFilePath(tempFilename);
|
2023-07-30 14:56:24 -04:00
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
QStringList convertedHashes;
|
|
|
|
for (uint64_t i = 0; i < patch.hash_count; i++) {
|
|
|
|
convertedHashes.push_back(QLatin1String(patch.hashes[i]));
|
|
|
|
}
|
|
|
|
|
|
|
|
const QueuedPatch queuedPatch{.name = QLatin1String(patch.version),
|
|
|
|
.repository = repository,
|
|
|
|
.version = QLatin1String(patch.version),
|
2023-12-17 10:27:07 -05:00
|
|
|
.path = patchPath,
|
2024-08-22 20:42:35 -04:00
|
|
|
.hashes = convertedHashes,
|
|
|
|
.hashBlockSize = patch.hash_block_size,
|
2023-12-17 10:27:07 -05:00
|
|
|
.length = patch.length,
|
|
|
|
.isBoot = isBoot()};
|
2023-10-04 11:09:50 -04:00
|
|
|
|
2023-10-08 18:02:02 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "Adding a queued patch:";
|
2024-08-22 20:42:35 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "- Repository or is boot:" << (isBoot() ? QStringLiteral("boot") : repository);
|
2023-10-08 18:02:02 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "- Version:" << patch.version;
|
|
|
|
qDebug(ASTRA_PATCHER) << "- Downloaded Path:" << patchPath;
|
|
|
|
qDebug(ASTRA_PATCHER) << "- Hashes:" << patch.hashes;
|
2024-08-22 20:42:35 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "- Hash Block Size:" << patch.hash_block_size;
|
2023-10-08 18:02:02 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "- Length:" << patch.length;
|
2023-07-30 14:56:24 -04:00
|
|
|
|
2023-10-04 11:09:50 -04:00
|
|
|
m_patchQueue[ourIndex] = queuedPatch;
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2023-09-16 20:30:34 -04:00
|
|
|
if (!QFile::exists(patchPath)) {
|
2024-06-27 22:27:21 -04:00
|
|
|
// make sure to remove any previous attempts
|
|
|
|
if (QFile::exists(tempPatchPath)) {
|
|
|
|
QFile::remove(tempPatchPath);
|
|
|
|
}
|
|
|
|
|
2024-08-22 20:42:35 -04:00
|
|
|
const auto patchRequest = QNetworkRequest(QUrl(QLatin1String(patch.url)));
|
2023-10-08 18:02:02 -04:00
|
|
|
Utility::printRequest(QStringLiteral("GET"), patchRequest);
|
|
|
|
|
2023-10-11 13:34:43 -04:00
|
|
|
auto patchReply = m_launcher.mgr()->get(patchRequest);
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
connect(patchReply, &QNetworkReply::downloadProgress, this, [this, ourIndex](const int received, const int total) {
|
2023-12-17 10:27:07 -05:00
|
|
|
Q_UNUSED(total)
|
2023-10-11 17:45:02 -04:00
|
|
|
updateDownloadProgress(ourIndex, received);
|
2023-09-16 20:30:34 -04:00
|
|
|
});
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2024-06-26 23:43:06 -04:00
|
|
|
connect(patchReply, &QNetworkReply::readyRead, this, [this, tempPatchPath, patchReply] {
|
|
|
|
// TODO: don't open the file each time we recieve data
|
|
|
|
QFile file(tempPatchPath);
|
|
|
|
file.open(QIODevice::WriteOnly | QIODevice::Append);
|
2023-09-16 20:30:34 -04:00
|
|
|
file.write(patchReply->readAll());
|
|
|
|
file.close();
|
2024-06-26 23:43:06 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
synchronizer.addFuture(QtFuture::connect(patchReply, &QNetworkReply::finished).then([this, ourIndex, patchPath, tempPatchPath] {
|
|
|
|
qDebug(ASTRA_PATCHER) << "Downloaded to" << patchPath;
|
|
|
|
|
|
|
|
QDir().rename(tempPatchPath, patchPath);
|
2023-10-11 17:45:02 -04:00
|
|
|
|
|
|
|
QMutexLocker locker(&m_finishedPatchesMutex);
|
|
|
|
m_finishedPatches++;
|
|
|
|
m_patchQueue[ourIndex].downloaded = true;
|
|
|
|
|
|
|
|
updateMessage();
|
2023-09-16 20:30:34 -04:00
|
|
|
}));
|
2023-09-16 20:12:42 -04:00
|
|
|
} else {
|
2023-10-11 17:45:02 -04:00
|
|
|
m_patchQueue[ourIndex].downloaded = true;
|
|
|
|
m_finishedPatches++;
|
2024-08-22 20:42:35 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "Found existing patch: " << patch.version;
|
2022-07-20 18:00:42 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-16 20:12:42 -04:00
|
|
|
co_await QtConcurrent::run([&synchronizer] {
|
|
|
|
synchronizer.waitForFinished();
|
|
|
|
});
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2023-09-16 20:12:42 -04:00
|
|
|
// This must happen synchronously
|
2023-12-17 13:38:23 -05:00
|
|
|
int i = 0;
|
2023-09-16 21:18:50 -04:00
|
|
|
for (const auto &patch : m_patchQueue) {
|
2023-10-11 17:45:02 -04:00
|
|
|
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));
|
2023-12-20 17:09:17 -05:00
|
|
|
Q_EMIT m_launcher.stageDeterminate(0, static_cast<int>(m_patchQueue.size()), i);
|
2023-09-16 20:30:34 -04:00
|
|
|
|
2023-10-11 17:45:02 -04:00
|
|
|
co_await QtConcurrent::run([this, patch] {
|
|
|
|
processPatch(patch);
|
|
|
|
});
|
2022-07-20 18:00:42 -04:00
|
|
|
}
|
2023-09-17 18:43:58 -04:00
|
|
|
|
|
|
|
co_return true;
|
2022-07-20 18:00:42 -04:00
|
|
|
}
|
|
|
|
|
2023-07-30 08:49:34 -04:00
|
|
|
void Patcher::processPatch(const QueuedPatch &patch)
|
|
|
|
{
|
2023-07-30 14:56:24 -04:00
|
|
|
// Perform hash checking
|
|
|
|
if (!patch.hashes.isEmpty()) {
|
|
|
|
auto f = QFile(patch.path);
|
|
|
|
f.open(QIODevice::ReadOnly);
|
|
|
|
|
2023-12-20 17:09:17 -05:00
|
|
|
qDebug(ASTRA_PATCHER) << "Installing" << patch.path;
|
|
|
|
|
2024-06-27 22:27:21 -04:00
|
|
|
if (patch.length != f.size()) {
|
|
|
|
f.remove();
|
2024-06-27 22:48:26 -04:00
|
|
|
qCritical(ASTRA_PATCHER) << patch.path << "has the wrong size.";
|
2024-06-27 22:27:21 -04:00
|
|
|
Q_EMIT m_launcher.miscError(i18n("Patch %1 is the wrong size. The downloaded patch has been discarded, please log in again.", patch.name));
|
|
|
|
return;
|
|
|
|
}
|
2023-07-30 14:56:24 -04:00
|
|
|
|
|
|
|
const int parts = std::ceil(static_cast<double>(patch.length) / static_cast<double>(patch.hashBlockSize));
|
2023-09-17 09:27:41 -04:00
|
|
|
|
2023-07-30 14:56:24 -04:00
|
|
|
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);
|
|
|
|
|
2024-06-27 22:27:21 -04:00
|
|
|
if (QString::fromUtf8(hash.result().toHex()) != patch.hashes[i]) {
|
|
|
|
f.remove();
|
2024-06-27 22:48:26 -04:00
|
|
|
qCritical(ASTRA_PATCHER) << patch.path << "failed the hash check.";
|
2024-06-27 22:27:21 -04:00
|
|
|
Q_EMIT m_launcher.miscError(i18n("Patch %1 failed the hash check. The downloaded patch has been discarded, please log in again.", patch.name));
|
|
|
|
return;
|
|
|
|
}
|
2023-07-30 14:56:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-27 22:27:21 -04:00
|
|
|
bool res;
|
2022-08-15 11:14:37 -04:00
|
|
|
if (isBoot()) {
|
2024-06-27 22:27:21 -04:00
|
|
|
res = physis_bootdata_apply_patch(m_bootData, patch.path.toStdString().c_str());
|
2022-08-09 22:44:10 -04:00
|
|
|
} else {
|
2024-06-27 22:27:21 -04:00
|
|
|
res = physis_gamedata_apply_patch(m_gameData, patch.path.toStdString().c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!res) {
|
2024-06-27 22:48:26 -04:00
|
|
|
qCritical(ASTRA_PATCHER) << "Failed to install" << patch.path << "to" << (isBoot() ? QStringLiteral("boot") : patch.repository);
|
2024-06-27 22:29:29 -04:00
|
|
|
Q_EMIT m_launcher.miscError(i18n("Patch %1 failed to apply. The game is now in an invalid state and must be immediately repaired.", patch.name));
|
2024-06-27 22:27:21 -04:00
|
|
|
return;
|
2022-08-09 22:44:10 -04:00
|
|
|
}
|
2022-07-20 18:00:42 -04:00
|
|
|
|
2023-10-08 18:02:02 -04:00
|
|
|
qDebug(ASTRA_PATCHER) << "Installed" << patch.path << "to" << (isBoot() ? QStringLiteral("boot") : patch.repository);
|
2023-10-04 11:09:50 -04:00
|
|
|
|
2022-07-20 18:00:42 -04:00
|
|
|
QString verFilePath;
|
2022-08-15 11:14:37 -04:00
|
|
|
if (isBoot()) {
|
2023-09-16 21:18:50 -04:00
|
|
|
verFilePath = m_baseDirectory + QStringLiteral("/ffxivboot.ver");
|
2022-07-20 18:00:42 -04:00
|
|
|
} else {
|
2023-12-17 12:01:28 -05:00
|
|
|
if (patch.repository == "game"_L1) {
|
2023-09-16 21:18:50 -04:00
|
|
|
verFilePath = m_baseDirectory + QStringLiteral("/ffxivgame.ver");
|
2022-07-20 18:00:42 -04:00
|
|
|
} else {
|
2024-06-27 20:31:19 -04:00
|
|
|
const QString sqPackDir = m_baseDirectory + QStringLiteral("/sqpack/") + patch.repository + QStringLiteral("/");
|
|
|
|
Utility::createPathIfNeeded(sqPackDir);
|
|
|
|
verFilePath = sqPackDir + patch.repository + QStringLiteral(".ver");
|
2022-07-20 18:00:42 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-21 20:28:41 -05:00
|
|
|
Utility::writeVersion(verFilePath, patch.version);
|
2024-06-29 11:00:48 -04:00
|
|
|
|
|
|
|
if (!m_launcher.settings()->keepPatches()) {
|
|
|
|
QFile::remove(patch.path);
|
|
|
|
}
|
2022-07-20 18:00:42 -04:00
|
|
|
}
|
2023-08-18 14:52:06 -04:00
|
|
|
|
|
|
|
void Patcher::setupDirectories()
|
|
|
|
{
|
|
|
|
QDir dataDir;
|
2024-06-29 11:00:48 -04:00
|
|
|
dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
2023-08-18 14:52:06 -04:00
|
|
|
|
2023-12-21 20:53:24 -05:00
|
|
|
m_patchesDir.setPath(dataDir.absoluteFilePath(QStringLiteral("patch")));
|
2024-08-22 20:42:35 -04:00
|
|
|
m_patchesDirStorageInfo = QStorageInfo(m_patchesDir);
|
2023-08-18 14:52:06 -04:00
|
|
|
}
|
2023-09-16 20:30:34 -04:00
|
|
|
|
|
|
|
QString Patcher::getBaseString() const
|
|
|
|
{
|
|
|
|
if (isBoot()) {
|
|
|
|
return i18n("FINAL FANTASY XIV Update/Launcher");
|
|
|
|
} else {
|
|
|
|
return i18n("FINAL FANTASY XIV Game");
|
|
|
|
}
|
|
|
|
}
|
2023-10-11 17:45:02 -04:00
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
void Patcher::updateDownloadProgress(const int index, const int received)
|
2023-10-11 17:45:02 -04:00
|
|
|
{
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2024-07-04 20:53:06 -04:00
|
|
|
const float progress = (static_cast<float>(patch.bytesDownloaded) / static_cast<float>(patch.length)) * 100.0f;
|
2023-12-17 10:09:01 -05:00
|
|
|
const QString progressStr = QStringLiteral("%1").arg(progress, 1, 'f', 1, QLatin1Char('0'));
|
2023-10-11 17:45:02 -04:00
|
|
|
|
|
|
|
Q_EMIT m_launcher.stageChanged(i18n("Downloading %1 - %2 [%3/%4]", repositoryName, patch.version, m_finishedPatches, m_remainingPatches),
|
|
|
|
i18n("%1%", progressStr));
|
2023-12-17 13:38:23 -05:00
|
|
|
Q_EMIT m_launcher.stageDeterminate(0, static_cast<int>(patch.length), static_cast<int>(patch.bytesDownloaded));
|
2023-10-11 17:45:02 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-17 11:12:13 -05:00
|
|
|
|
|
|
|
#include "moc_patcher.cpp"
|