1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-20 11:47:46 +00:00
astra/launcher/src/patcher.cpp

190 lines
6 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "patcher.h"
#include <KLocalizedString>
2022-08-15 11:14:37 -04:00
#include <QDir>
#include <QFile>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStandardPaths>
2023-09-16 20:12:42 -04:00
#include <QtConcurrent>
#include <physis.hpp>
2023-09-16 20:12:42 -04:00
#include <qcorofuture.h>
#include "launchercore.h"
2023-09-16 21:18:50 -04:00
Patcher::Patcher(LauncherCore &launcher, const QString &baseDirectory, BootData &bootData, QObject *parent)
: QObject(parent)
2023-09-16 21:18:50 -04:00
, m_baseDirectory(baseDirectory)
, m_bootData(&bootData)
, m_launcher(launcher)
{
setupDirectories();
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)
: QObject(parent)
2023-09-16 21:18:50 -04:00
, m_baseDirectory(baseDirectory)
, m_gameData(&gameData)
, m_launcher(launcher)
{
setupDirectories();
Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version.", getBaseString()));
}
2023-09-16 21:18:50 -04:00
QCoro::Task<> Patcher::patch(const QString &patchList)
{
2022-08-15 11:14:37 -04:00
if (patchList.isEmpty()) {
2023-09-16 20:12:42 -04:00
co_return;
}
2023-09-16 20:30:34 -04:00
Q_EMIT m_launcher.stageIndeterminate();
Q_EMIT m_launcher.stageChanged(i18n("Checking %1 version.", getBaseString()));
2023-09-16 20:12:42 -04:00
const QStringList parts = patchList.split("\r\n");
2023-09-16 21:18:50 -04:00
m_remainingPatches = parts.size() - 7;
m_patchQueue.resize(m_remainingPatches);
2023-09-16 20:12:42 -04:00
QFutureSynchronizer<void> synchronizer;
2023-09-16 20:12:42 -04:00
int patchIndex = 0;
2023-09-16 20:12:42 -04:00
for (int i = 5; i < parts.size() - 2; i++) {
2023-09-16 20:14:37 -04:00
const QStringList patchParts = parts[i].split(QLatin1Char('\t'));
2023-09-16 20:12:42 -04:00
const int length = patchParts[0].toInt();
2023-09-16 20:30:34 -04:00
const int ourIndex = patchIndex++;
2023-09-16 20:30:34 -04:00
const QString &version = patchParts[4];
2023-09-16 20:12:42 -04:00
const long hashBlockSize = patchParts.size() == 9 ? patchParts[6].toLong() : 0;
2023-09-16 20:30:34 -04:00
const QString &name = version;
2023-09-16 20:14:37 -04:00
const QStringList hashes = patchParts.size() == 9 ? (patchParts[7].split(QLatin1Char(','))) : QStringList();
2023-09-16 20:30:34 -04:00
const QString &url = patchParts[patchParts.size() == 9 ? 8 : 5];
2023-09-16 20:12:42 -04:00
const QString filename = QStringLiteral("%1.patch").arg(name);
2023-09-16 20:14:37 -04:00
auto url_parts = url.split(QLatin1Char('/'));
2023-09-16 20:12:42 -04:00
const QString repository = url_parts[url_parts.size() - 3];
2023-09-16 21:18:50 -04:00
const QDir repositoryDir = m_patchesDir.absoluteFilePath(repository);
2023-09-16 20:12:42 -04:00
if (!QDir().exists(repositoryDir.absolutePath()))
QDir().mkpath(repositoryDir.absolutePath());
2023-09-16 20:12:42 -04:00
const QString patchPath = repositoryDir.absoluteFilePath(filename);
2023-09-16 20:30:34 -04:00
const QueuedPatch patch{name, repository, version, patchPath, hashes, hashBlockSize, length, isBoot()};
2023-09-16 21:18:50 -04:00
m_patchQueue[ourIndex] = patch;
2023-09-16 20:30:34 -04:00
if (!QFile::exists(patchPath)) {
2023-09-16 21:18:50 -04:00
auto patchReply = m_launcher.mgr->get(QNetworkRequest(url));
2023-09-16 21:18:50 -04:00
connect(patchReply, &QNetworkReply::downloadProgress, this, [this, patch](int received, int total) {
2023-09-16 20:30:34 -04:00
Q_EMIT m_launcher.stageChanged(i18n("Updating %1.\nDownloading %2", getBaseString(), patch.getVersion()));
Q_EMIT m_launcher.stageDeterminate(0, total, received);
});
2023-09-16 20:30:34 -04:00
synchronizer.addFuture(QtFuture::connect(patchReply, &QNetworkReply::finished).then([patchPath, patchReply] {
QFile file(patchPath);
file.open(QIODevice::WriteOnly);
file.write(patchReply->readAll());
file.close();
}));
2023-09-16 20:12:42 -04:00
} else {
qDebug() << "Found existing patch: " << name;
}
}
2023-09-16 20:12:42 -04:00
co_await QtConcurrent::run([&synchronizer] {
synchronizer.waitForFinished();
});
2023-09-16 20:12:42 -04:00
// This must happen synchronously
size_t i = 0;
2023-09-16 21:18:50 -04:00
for (const auto &patch : m_patchQueue) {
2023-09-16 20:30:34 -04:00
Q_EMIT m_launcher.stageChanged(i18n("Updating %1.\nInstalling %2", getBaseString(), patch.getVersion()));
2023-09-16 21:18:50 -04:00
Q_EMIT m_launcher.stageDeterminate(0, m_patchQueue.size(), i++);
2023-09-16 20:30:34 -04:00
2023-09-16 20:12:42 -04:00
processPatch(patch);
}
}
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<double>(patch.length) / static_cast<double>(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(hash.result().toHex() == patch.hashes[i]);
}
}
2022-08-15 11:14:37 -04:00
if (isBoot()) {
2023-09-16 21:18:50 -04:00
physis_bootdata_apply_patch(m_bootData, patch.path.toStdString().c_str());
2022-08-09 22:44:10 -04:00
} else {
2023-09-16 21:18:50 -04:00
physis_gamedata_apply_patch(m_gameData, patch.path.toStdString().c_str());
2022-08-09 22:44:10 -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");
} else {
2023-09-16 20:14:37 -04:00
if (patch.repository == QLatin1String("game")) {
2023-09-16 21:18:50 -04:00
verFilePath = m_baseDirectory + QStringLiteral("/ffxivgame.ver");
} else {
2023-09-16 21:18:50 -04:00
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.keepPatches()) {
dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
} else {
dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
}
2023-09-16 21:18:50 -04:00
m_patchesDir.setPath(dataDir.absoluteFilePath(QStringLiteral("patches")));
}
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");
}
}