From 3d326746636b3f71b1613b684571788da8ab687d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 20 Jul 2022 18:00:42 -0400 Subject: [PATCH] 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. --- launcher/core/CMakeLists.txt | 6 +- launcher/core/include/patcher.h | 37 +++++++ launcher/core/include/squareboot.h | 4 +- launcher/core/include/squarelauncher.h | 3 + launcher/core/src/patcher.cpp | 136 +++++++++++++++++++++++++ launcher/core/src/squareboot.cpp | 69 ++----------- launcher/core/src/squarelauncher.cpp | 23 +++-- 7 files changed, 206 insertions(+), 72 deletions(-) create mode 100644 launcher/core/include/patcher.h create mode 100644 launcher/core/src/patcher.cpp diff --git a/launcher/core/CMakeLists.txt b/launcher/core/CMakeLists.txt index e5b4020..d7fd47e 100644 --- a/launcher/core/CMakeLists.txt +++ b/launcher/core/CMakeLists.txt @@ -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} diff --git a/launcher/core/include/patcher.h b/launcher/core/include/patcher.h new file mode 100644 index 0000000..180e20b --- /dev/null +++ b/launcher/core/include/patcher.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +// 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 patchQueue; + + bool isBoot = false; + QString baseDirectory; + + QProgressDialog* dialog = nullptr; + + int remainingPatches = -1; +}; \ No newline at end of file diff --git a/launcher/core/include/squareboot.h b/launcher/core/include/squareboot.h index 21db8ed..11feb0b 100644 --- a/launcher/core/include/squareboot.h +++ b/launcher/core/include/squareboot.h @@ -1,6 +1,6 @@ #pragma once -#include +#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; diff --git a/launcher/core/include/squarelauncher.h b/launcher/core/include/squarelauncher.h index db43adb..fe690b1 100644 --- a/launcher/core/include/squarelauncher.h +++ b/launcher/core/include/squarelauncher.h @@ -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; diff --git a/launcher/core/src/patcher.cpp b/launcher/core/src/patcher.cpp new file mode 100644 index 0000000..245532c --- /dev/null +++ b/launcher/core/src/patcher.cpp @@ -0,0 +1,136 @@ +#include "patcher.h" +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/launcher/core/src/squareboot.cpp b/launcher/core/src/squareboot.cpp index 068ecfd..ead50be 100644 --- a/launcher/core/src/squareboot.cpp +++ b/launcher/core/src/squareboot.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include @@ -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); }); } diff --git a/launcher/core/src/squarelauncher.cpp b/launcher/core/src/squarelauncher.cpp index 77e3f55..25bd944 100644 --- a/launcher/core/src/squarelauncher.cpp +++ b/launcher/core/src/squarelauncher.cpp @@ -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);