1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-23 21:07:45 +00:00

Add game patching support

Yes it's finally here! It's been tested to update the game from the base
2012 version all the way up to patch 6.18. This also works across all
expansions, and the previous boot patches that were supported before.

However, the patcher dialog is in need of an update, as patching happens
on the same thread so the entire application freezes.
This commit is contained in:
Joshua Goins 2022-07-20 18:00:42 -04:00
parent a48b6d630d
commit 3d32674663
7 changed files with 206 additions and 72 deletions

View file

@ -7,7 +7,8 @@ set(HEADERS
include/launchercore.h
include/sapphirelauncher.h
include/squareboot.h
include/squarelauncher.h)
include/squarelauncher.h
include/patcher.h)
set(SRC
src/dxvkinstaller.cpp
@ -17,7 +18,8 @@ set(SRC
src/launchercore.cpp
src/sapphirelauncher.cpp
src/squareboot.cpp
src/squarelauncher.cpp)
src/squarelauncher.cpp
src/patcher.cpp)
if(ENABLE_WATCHDOG)
set(HEADERS ${HEADERS}

View file

@ -0,0 +1,37 @@
#pragma once
#include <QString>
#include <QProgressDialog>
#include <QNetworkAccessManager>
// General-purpose patcher routine. It opens a nice dialog box, handles downloading
// and processing patches.
class Patcher : public QObject {
Q_OBJECT
public:
// isBoot is used for telling the patcher that you're reading boot patches, which for some reason has a different patchlist format.
Patcher(bool isBoot, QString baseDirectory);
void processPatchList(QNetworkAccessManager& mgr, QString patchList);
signals:
void done();
private:
void checkIfDone();
struct QueuedPatch {
QString name, repository, version, path;
};
void processPatch(QueuedPatch patch);
QVector<QueuedPatch> patchQueue;
bool isBoot = false;
QString baseDirectory;
QProgressDialog* dialog = nullptr;
int remainingPatches = -1;
};

View file

@ -1,6 +1,6 @@
#pragma once
#include <QProgressDialog>
#include "patcher.h"
class SquareLauncher;
class LauncherCore;
@ -16,7 +16,7 @@ public:
void bootCheck(const LoginInformation& info);
private:
QProgressDialog* dialog;
Patcher* patcher = nullptr;
LauncherCore& window;
SquareLauncher& launcher;

View file

@ -1,6 +1,7 @@
#pragma once
#include "launchercore.h"
#include "patcher.h"
class SquareLauncher : public QObject {
Q_OBJECT
@ -16,6 +17,8 @@ public:
private:
QString getBootHash(const LoginInformation& info);
Patcher* patcher = nullptr;
QString stored, SID, username;
LoginAuth auth;

View file

@ -0,0 +1,136 @@
#include "patcher.h"
#include <QFile>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QStandardPaths>
#include <physis.hpp>
#include <QDir>
Patcher::Patcher(bool isBoot, QString baseDirectory) : isBoot(isBoot), baseDirectory(baseDirectory) {
dialog = new QProgressDialog();
if(isBoot) {
dialog->setLabelText("Checking the FINAL FANTASY XIV Updater/Launcher version.");
} else {
dialog->setLabelText("Checking the FINAL FANTASY XIV Game version.");
}
dialog->show();
}
void Patcher::processPatchList(QNetworkAccessManager& mgr, QString patchList) {
if(patchList.isEmpty()) {
dialog->hide();
emit done();
} else {
if(isBoot) {
dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version.");
} else {
dialog->setLabelText("Updating the FINAL FANTASY XIV Game version.");
}
const QStringList parts = patchList.split(QRegExp("\n|\r\n|\r"));
remainingPatches = parts.size() - 7;
for(int i = 5; i < parts.size() - 2; i++) {
const QStringList patchParts = parts[i].split("\t");
const int length = patchParts[0].toInt();
QString name, url, version, repository;
if (isBoot) {
name = patchParts[4];
url = patchParts[5];
version = name;
} else {
url = patchParts[8];
version = patchParts[4];
name = url.split('/').last().remove(".patch");
}
auto url_parts = url.split('/');
repository = url_parts[url_parts.size() - 3];
if(isBoot) {
dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version.\nDownloading ffxivboot - " + version);
} else {
dialog->setLabelText("Updating the FINAL FANTASY XIV Game version.\nDownloading " + repository + " - " + version);
}
dialog->setMinimum(0);
dialog->setMaximum(length);
const QString patchesDir =
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository;
if(!QDir().exists(patchesDir))
QDir().mkdir(patchesDir);
if(!QFile::exists(patchesDir + "/" + name + ".patch")) {
QNetworkRequest patchRequest(url);
auto patchReply = mgr.get(patchRequest);
connect(patchReply, &QNetworkReply::downloadProgress, [=](int recieved, int total) {
dialog->setValue(recieved);
});
connect(patchReply, &QNetworkReply::finished, [=] {
QFile file(patchesDir + "/" + name + ".patch");
file.open(QIODevice::WriteOnly);
file.write(patchReply->readAll());
file.close();
auto patch_path = patchesDir + "/" + name + ".patch";
patchQueue.push_back({name, repository, version, patch_path});
remainingPatches--;
checkIfDone();
});
} else {
patchQueue.push_back({name, repository, version, patchesDir + "/" + name + ".patch"});
remainingPatches--;
checkIfDone();
}
}
}
}
void Patcher::checkIfDone() {
if(remainingPatches <= 0) {
for (auto patch : patchQueue) {
processPatch(patch);
}
patchQueue.clear();
dialog->hide();
emit done();
}
}
void Patcher::processPatch(QueuedPatch patch) {
auto data_path = baseDirectory.toStdString();
physis_patch_process(data_path.c_str(), patch.path.toStdString().c_str());
QString verFilePath;
if(isBoot) {
verFilePath = baseDirectory + "/ffxivboot.ver";
} else {
if(patch.repository == "game") {
verFilePath = baseDirectory + "/ffxivgame.ver";
} else {
verFilePath = baseDirectory + "/sqpack/" + patch.repository + "/" + patch.repository + ".ver";
}
}
QFile verFile(verFilePath);
verFile.open(QIODevice::WriteOnly | QIODevice::Text);
verFile.write(patch.version.toUtf8());
verFile.close();
}

View file

@ -6,7 +6,7 @@
#include <QPushButton>
#include <QStandardPaths>
#include <QFile>
#include <patch.h>
#include <physis.hpp>
#include <QJsonDocument>
#include <QJsonObject>
@ -17,9 +17,12 @@ SquareBoot::SquareBoot(LauncherCore& window, SquareLauncher& launcher) : window(
}
void SquareBoot::bootCheck(const LoginInformation& info) {
dialog = new QProgressDialog();
dialog->setLabelText("Checking the FINAL FANTASY XIV Updater/Launcher version.");
dialog->show();
patcher = new Patcher(true, info.settings->gamePath + "/boot");
connect(patcher, &Patcher::done, [=, &info] {
window.readGameVersion();
launcher.getStored(info);
});
QUrlQuery query;
query.addQueryItem("time", QDateTime::currentDateTimeUtc().toString("yyyy-MM-dd-HH-mm"));
@ -43,63 +46,7 @@ void SquareBoot::bootCheck(const LoginInformation& info) {
connect(reply, &QNetworkReply::finished, [=, &info] {
const QString response = reply->readAll();
if(response.isEmpty()) {
dialog->hide();
launcher.getStored(info);
} else {
dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version.");
// TODO: move this out into a dedicated function, we need to use this for regular game patches later on
// TODO: create a nice progress window like ffxivboot has
// TODO: improve flow when updating boot, maybe do at launch ala official launcher?
const QStringList parts = response.split(QRegExp("\n|\r\n|\r"));
// patch list starts at line 5
for(int i = 5; i < parts.size() - 2; i++) {
const QStringList patchParts = parts[i].split("\t");
const int length = patchParts[0].toInt();
const int version = patchParts[4].toInt();
const int hashType = patchParts[5].toInt();
QString name = patchParts[4];
QString url = patchParts[5];
// TODO: show bytes recieved/total in the progress window, and speed
dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version.\nDownloading ffxivboot - " + name);
dialog->setMinimum(0);
dialog->setMaximum(length);
QNetworkRequest patchRequest(url);
auto patchReply = window.mgr->get(patchRequest);
connect(patchReply, &QNetworkReply::downloadProgress, [=](int recieved, int total) {
dialog->setValue(recieved);
});
connect(patchReply, &QNetworkReply::finished, [=, &info] {
const QString dataDir =
QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QFile file(dataDir + "/" + name + ".patch");
file.open(QIODevice::WriteOnly);
file.write(patchReply->readAll());
file.close();
// TODO: we really a dedicated .ver writing/reading class
QFile verFile(info.settings->gamePath + "/boot/ffxivboot.ver");
verFile.open(QIODevice::WriteOnly | QIODevice::Text);
verFile.write(name.toUtf8());
verFile.close();
processPatch((dataDir + "/" + name + ".patch").toStdString(), (info.settings->gamePath + "/boot").toStdString());
info.settings->bootVersion = name;
launcher.getStored(info);
});
}
}
patcher->processPatchList(*window.mgr, response);
});
}

View file

@ -183,17 +183,26 @@ void SquareLauncher::registerSession(const LoginInformation& info) {
auto reply = window.mgr->post(request, report.toUtf8());
connect(reply, &QNetworkReply::finished, [=, &info] {
if(reply->rawHeaderList().contains("X-Patch-Unique-Id")) {
auth.SID = reply->rawHeader("X-Patch-Unique-Id");
QString body = reply->readAll();
patcher = new Patcher(false, info.settings->gamePath + "/game");
connect(patcher, &Patcher::done, [=, &info] {
window.readGameVersion();
auth.SID = reply->rawHeader("X-Patch-Unique-Id");
#ifdef ENABLE_WATCHDOG
if(info.settings->enableWatchdog) {
window.watchdog->launchGame(*info.settings, auth);
} else {
window.launchGame(*info.settings, auth);
}
if(info.settings->enableWatchdog) {
window.watchdog->launchGame(*info.settings, auth);
} else {
window.launchGame(*info.settings, auth);
}
#else
window.launchGame(*info.settings, auth);
window.launchGame(*info.settings, auth);
#endif
});
patcher->processPatchList(*window.mgr, body);
} else {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Failed the anti-tamper check. Please restore your game to the original state or update the game.");
window.addUpdateButtons(*info.settings, *messageBox);