diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h index a538de4..1bc3ea3 100755 --- a/launcher/include/launchercore.h +++ b/launcher/include/launchercore.h @@ -149,6 +149,8 @@ signals: void showNewsListChanged(); void loginError(QString message); void stageChanged(QString message); + void stageIndeterminate(); + void stageDeterminate(int min, int max, int value); void newsChanged(); private: diff --git a/launcher/include/patcher.h b/launcher/include/patcher.h index 962b52e..2847e1a 100644 --- a/launcher/include/patcher.h +++ b/launcher/include/patcher.h @@ -4,14 +4,16 @@ #include #include +class LauncherCore; + // General-purpose patcher routine. It opens a nice dialog box, handles downloading // and processing patches. class Patcher : public QObject { Q_OBJECT public: - Patcher(QString baseDirectory, GameData *game_data, QObject *parent = nullptr); - Patcher(QString baseDirectory, BootData *game_data, QObject *parent = nullptr); + Patcher(LauncherCore &launcher, QString baseDirectory, GameData *game_data, QObject *parent = nullptr); + Patcher(LauncherCore &launcher, QString baseDirectory, BootData *game_data, QObject *parent = nullptr); void processPatchList(QNetworkAccessManager &mgr, const QString &patchList); @@ -28,6 +30,9 @@ private: struct QueuedPatch { QString name, repository, version, path; + QStringList hashes; + long hashBlockSize; + long length; }; void processPatch(const QueuedPatch &patch); @@ -39,4 +44,6 @@ private: GameData *game_data = nullptr; int remainingPatches = -1; + + LauncherCore &m_launcher; }; \ No newline at end of file diff --git a/launcher/src/patcher.cpp b/launcher/src/patcher.cpp index 18f777d..612f4df 100644 --- a/launcher/src/patcher.cpp +++ b/launcher/src/patcher.cpp @@ -1,5 +1,6 @@ #include "patcher.h" +#include #include #include #include @@ -9,78 +10,63 @@ #include #include -Patcher::Patcher(QString baseDirectory, BootData *boot_data, QObject *parent) +#include "launchercore.h" + +Patcher::Patcher(LauncherCore &launcher, QString baseDirectory, BootData *boot_data, QObject *parent) : QObject(parent) , baseDirectory(std::move(baseDirectory)) , boot_data(boot_data) + , m_launcher(launcher) { - /*dialog = new QProgressDialog(); - dialog->setLabelText("Checking the FINAL FANTASY XIV Updater/Launcher version."); - - dialog->show();*/ + Q_EMIT m_launcher.stageChanged(i18n("Checking the FINAL FANTASY XIV Updater/Launcher version.")); } -Patcher::Patcher(QString baseDirectory, GameData *game_data, QObject *parent) +Patcher::Patcher(LauncherCore &launcher, QString baseDirectory, GameData *game_data, QObject *parent) : QObject(parent) , baseDirectory(std::move(baseDirectory)) , game_data(game_data) + , m_launcher(launcher) { - /*dialog = new QProgressDialog(); - dialog->setLabelText("Checking the FINAL FANTASY XIV Game version."); - - dialog->show();*/ + Q_EMIT m_launcher.stageChanged(i18n("Checking the FINAL FANTASY XIV Game version.")); } void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchList) { if (patchList.isEmpty()) { - // dialog->hide(); - emit done(); } else { if (isBoot()) { - // dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version."); + Q_EMIT m_launcher.stageIndeterminate(); + Q_EMIT m_launcher.stageChanged(i18n("Checking the FINAL FANTASY XIV Update/Launcher version.")); } else { - // dialog->setLabelText("Updating the FINAL FANTASY XIV Game version."); + Q_EMIT m_launcher.stageIndeterminate(); + Q_EMIT m_launcher.stageChanged(i18n("Checking the FINAL FANTASY XIV Game version.")); } const QStringList parts = patchList.split("\r\n"); remainingPatches = parts.size() - 7; + patchQueue.resize(remainingPatches); + + int patchIndex = 0; for (int i = 5; i < parts.size() - 2; i++) { const QStringList patchParts = parts[i].split("\t"); const int length = patchParts[0].toInt(); - Q_UNUSED(length) + int ourIndex = patchIndex++; - QString name, url, version, repository; + const QString version = patchParts[4]; + const long hashBlockSize = patchParts.size() == 9 ? patchParts[6].toLong() : 0; - if (isBoot()) { - name = patchParts[4]; - url = patchParts[5]; - version = name; - } else { - url = patchParts[8]; - version = patchParts[4]; - name = url.split('/').last().remove(".patch"); - } + const QString name = version; + const QStringList hashes = patchParts.size() == 9 ? (patchParts[7].split(',')) : QStringList(); + const QString url = patchParts[patchParts.size() == 9 ? 8 : 5]; qDebug() << "Parsed patch name: " << name; 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 repository = url_parts[url_parts.size() - 3]; const QString patchesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository; @@ -92,29 +78,37 @@ void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchL QNetworkRequest patchRequest(url); auto patchReply = mgr.get(patchRequest); - connect(patchReply, &QNetworkReply::downloadProgress, [=](int recieved, int total) { - Q_UNUSED(recieved) + connect(patchReply, &QNetworkReply::downloadProgress, [this, repository, version, length](int recieved, int total) { Q_UNUSED(total) - // dialog->setValue(recieved); + + if (isBoot()) { + Q_EMIT m_launcher.stageChanged(i18n("Updating the FINAL FANTASY XIV Updater/Launcher version.\nDownloading ffxivboot - %1", version)); + } else { + Q_EMIT m_launcher.stageChanged(i18n("Updating the FINAL FANTASY XIV Game version.\nDownloading %1 - %2", repository, version)); + } + + Q_EMIT m_launcher.stageDeterminate(0, length, recieved); }); - connect(patchReply, &QNetworkReply::finished, [this, patchesDir, name, patchReply, repository, version] { - QFile file(patchesDir + "/" + name + ".patch"); - file.open(QIODevice::WriteOnly); - file.write(patchReply->readAll()); - file.close(); + connect(patchReply, + &QNetworkReply::finished, + [this, ourIndex, patchesDir, name, patchReply, repository, version, hashes, hashBlockSize, length] { + QFile file(patchesDir + "/" + name + ".patch"); + file.open(QIODevice::WriteOnly); + file.write(patchReply->readAll()); + file.close(); - auto patch_path = patchesDir + "/" + name + ".patch"; + auto patch_path = patchesDir + "/" + name + ".patch"; - patchQueue.push_back({name, repository, version, patch_path}); + patchQueue[ourIndex] = {name, repository, version, patch_path, hashes, hashBlockSize, length}; - remainingPatches--; - checkIfDone(); - }); + remainingPatches--; + checkIfDone(); + }); } else { qDebug() << "Found existing patch: " << name; - patchQueue.push_back({name, repository, version, patchesDir + "/" + name + ".patch"}); + patchQueue[ourIndex] = {name, repository, version, patchesDir + "/" + name + ".patch", hashes, hashBlockSize, length}; remainingPatches--; checkIfDone(); @@ -126,20 +120,51 @@ void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchL void Patcher::checkIfDone() { if (remainingPatches <= 0) { + if (isBoot()) { + Q_EMIT m_launcher.stageChanged(i18n("Applying updates to the FINAL FANTASY XIV Updater/Launcher.")); + } else { + Q_EMIT m_launcher.stageChanged(i18n("Applying updates to the FINAL FANTASY XIV Game.")); + } + + int i = 0; for (const auto &patch : patchQueue) { + Q_EMIT m_launcher.stageDeterminate(0, patchQueue.size(), i++); processPatch(patch); } patchQueue.clear(); - // dialog->hide(); - emit done(); } } 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(hash.result().toHex() == patch.hashes[i]); + } + } + if (isBoot()) { physis_bootdata_apply_patch(boot_data, patch.path.toStdString().c_str()); } else { diff --git a/launcher/src/squareboot.cpp b/launcher/src/squareboot.cpp index 99614ab..9a433c9 100644 --- a/launcher/src/squareboot.cpp +++ b/launcher/src/squareboot.cpp @@ -22,7 +22,7 @@ void SquareBoot::bootCheck(const LoginInformation &info) { Q_EMIT window.stageChanged(i18n("Checking for launcher updates...")); - patcher = new Patcher(info.profile->gamePath() + "/boot", info.profile->bootData, this); + patcher = new Patcher(window, info.profile->gamePath() + "/boot", info.profile->bootData, this); connect(patcher, &Patcher::done, [this, &info] { info.profile->readGameVersion(); diff --git a/launcher/src/squarelauncher.cpp b/launcher/src/squarelauncher.cpp index 41ca6eb..8f1d801 100644 --- a/launcher/src/squarelauncher.cpp +++ b/launcher/src/squarelauncher.cpp @@ -171,7 +171,7 @@ void SquareLauncher::registerSession(const LoginInformation &info) if (reply->rawHeaderList().contains("X-Patch-Unique-Id")) { QString body = reply->readAll(); - patcher = new Patcher(info.profile->gamePath() + "/game", info.profile->gameData, this); + patcher = new Patcher(window, info.profile->gamePath() + "/game", info.profile->gameData, this); connect(patcher, &Patcher::done, [this, &info, reply] { info.profile->readGameVersion(); diff --git a/launcher/ui/Pages/StatusPage.qml b/launcher/ui/Pages/StatusPage.qml index 5c1360d..be00e87 100644 --- a/launcher/ui/Pages/StatusPage.qml +++ b/launcher/ui/Pages/StatusPage.qml @@ -37,6 +37,17 @@ Kirigami.Page { placeholder.text = message } + function onStageIndeterminate() { + placeholder.determinate = false + } + + function onStageDeterminate(min, max, value) { + placeholder.determinate = true + placeholder.progressBar.value = value + placeholder.progressBar.from = min + placeholder.progressBar.to = max + } + function onLoginError(message) { errorDialog.subtitle = message errorDialog.open()