seasonal builds
This commit is contained in:
+188
-6
@@ -8,6 +8,7 @@
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <QStorageInfo>
|
||||
@@ -18,6 +19,7 @@ namespace {
|
||||
|
||||
const QString kIndexFile = QStringLiteral("index.json");
|
||||
const QString kStateSwitching = QStringLiteral("switching");
|
||||
const QString kStateApplying = QStringLiteral("applying");
|
||||
const QString kStateIdle = QStringLiteral("idle");
|
||||
|
||||
// Запас на архив: точный размер заранее неизвестен, но упасть на середине
|
||||
@@ -89,6 +91,8 @@ BuildSwitcher::BuildSwitcher(QObject *parent)
|
||||
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
|
||||
connect(m_worker, &BuildArchiveWorker::finished, this, &BuildSwitcher::onWorkerFinished);
|
||||
connect(m_worker, &BuildArchiveWorker::progress, this, &BuildSwitcher::onWorkerProgress);
|
||||
connect(m_worker, &BuildArchiveWorker::packEntries, this,
|
||||
[this](const QStringList &entries) { m_lastPackEntries = entries; });
|
||||
m_thread->start();
|
||||
|
||||
sweepTempArchives();
|
||||
@@ -110,7 +114,7 @@ double BuildSwitcher::fraction() const
|
||||
|
||||
QString BuildSwitcher::buildDir(int buildId) const
|
||||
{
|
||||
return LauncherPaths::buildStorageDir() + u'/' + QString::number(buildId);
|
||||
return LauncherPaths::buildDir(buildId);
|
||||
}
|
||||
|
||||
// На сборку — ровно один архив: имя с датой меняется от переключения к
|
||||
@@ -118,8 +122,14 @@ QString BuildSwitcher::buildDir(int buildId) const
|
||||
QString BuildSwitcher::archivePathOf(int buildId) const
|
||||
{
|
||||
const QDir dir(buildDir(buildId));
|
||||
// Рядом лежат служебные .tmp-*.zip и .pack-*.zip. На Unix они скрытые и в
|
||||
// выборку не попадают, на Windows — попадают, поэтому отсекаем по имени.
|
||||
const QStringList zips = dir.entryList({QStringLiteral("*.zip")}, QDir::Files, QDir::Time);
|
||||
return zips.isEmpty() ? QString() : dir.absoluteFilePath(zips.first());
|
||||
for (const QString &name : zips) {
|
||||
if (!name.startsWith(u'.'))
|
||||
return dir.absoluteFilePath(name);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void BuildSwitcher::setStage(const QString &stage)
|
||||
@@ -217,6 +227,15 @@ void BuildSwitcher::requestRestore(const QString &zipPath, const QString &gameDi
|
||||
Q_ARG(QString, zipPath), Q_ARG(QString, gameDir));
|
||||
}
|
||||
|
||||
void BuildSwitcher::requestApplyPack(const QString &zipPath,
|
||||
const QString &gameDir,
|
||||
const QStringList &remove)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_worker, "applyPack", Qt::QueuedConnection,
|
||||
Q_ARG(QString, zipPath), Q_ARG(QString, gameDir),
|
||||
Q_ARG(QStringList, remove), Q_ARG(QStringList, sharedTopLevel()));
|
||||
}
|
||||
|
||||
void BuildSwitcher::onWorkerProgress(int done, int total, const QString ¤tPath)
|
||||
{
|
||||
m_done = done;
|
||||
@@ -254,6 +273,17 @@ void BuildSwitcher::onWorkerFinished(bool ok, const QString &error)
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
case Applying:
|
||||
if (!ok) {
|
||||
// Архив проверяется целиком до первой записи, поэтому нулевой
|
||||
// счётчик означает, что в папке игры ничего не тронуто. Иначе
|
||||
// часть файлов уже заменена: отметка в index.json остаётся, и
|
||||
// следующий запуск доиграет раскатку из сохранённого пака.
|
||||
fail(error, /*gameDirIntact=*/m_done == 0);
|
||||
return;
|
||||
}
|
||||
finishApplying();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +308,9 @@ void BuildSwitcher::commitArchive()
|
||||
const QDir dir(buildDir(m_fromId));
|
||||
const QStringList zips = dir.entryList({QStringLiteral("*.zip")}, QDir::Files);
|
||||
for (const QString &name : zips) {
|
||||
if (dir.absoluteFilePath(name) != target)
|
||||
// Служебные файлы под удаление не попадают: в .pack-*.zip может лежать
|
||||
// пак недоигранной раскатки, без которого её нечем довести до конца.
|
||||
if (!name.startsWith(u'.') && dir.absoluteFilePath(name) != target)
|
||||
QFile::remove(dir.absoluteFilePath(name));
|
||||
}
|
||||
|
||||
@@ -323,6 +355,98 @@ void BuildSwitcher::beginRestoring()
|
||||
requestRestore(archive, m_gameDir);
|
||||
}
|
||||
|
||||
// ── Раскатка пака сезонной сборки ──────────────────────────────────────────
|
||||
|
||||
void BuildSwitcher::applyPack(int buildId,
|
||||
const QString &buildName,
|
||||
const QString &packZipPath,
|
||||
const QStringList &removeRelative,
|
||||
const QJsonObject ¬e,
|
||||
const QString &gameDir)
|
||||
{
|
||||
if (m_state != Idle) {
|
||||
emit failed(buildId, tr("Операция со сборками уже выполняется"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
QString rootError;
|
||||
if (!LauncherPaths::ensureRootExists(&rootError)) {
|
||||
emit failed(buildId, rootError, true);
|
||||
return;
|
||||
}
|
||||
if (!QFile::exists(packZipPath)) {
|
||||
emit failed(buildId,
|
||||
tr("Архив сборки не найден: %1")
|
||||
.arg(QDir::toNativeSeparators(packZipPath)),
|
||||
true);
|
||||
return;
|
||||
}
|
||||
|
||||
m_fromId = -1;
|
||||
m_toId = buildId;
|
||||
m_toName = buildName;
|
||||
m_gameDir = gameDir;
|
||||
m_packPath = packZipPath;
|
||||
m_packRemove = removeRelative;
|
||||
m_packNote = note;
|
||||
m_lastPackEntries.clear();
|
||||
m_lastPackNote = {};
|
||||
m_done = 0;
|
||||
m_total = 0;
|
||||
m_worker->clearCancel();
|
||||
|
||||
// Отметка ставится до первого разрушающего действия — как и при смене
|
||||
// сборки. Пак лежит рядом и переживёт перезапуск вместе с ней.
|
||||
QJsonObject index = readIndex();
|
||||
index.insert(QStringLiteral("state"), kStateApplying);
|
||||
index.insert(QStringLiteral("to"), buildId);
|
||||
index.insert(QStringLiteral("pack"), QFileInfo(packZipPath).fileName());
|
||||
index.insert(QStringLiteral("packName"), buildName);
|
||||
index.insert(QStringLiteral("packRemove"), QJsonArray::fromStringList(removeRelative));
|
||||
index.insert(QStringLiteral("packNote"), note);
|
||||
writeIndex(index);
|
||||
|
||||
m_state = Applying;
|
||||
beginApplying();
|
||||
}
|
||||
|
||||
void BuildSwitcher::beginApplying()
|
||||
{
|
||||
setStage(tr("Установка сборки «%1»").arg(m_toName));
|
||||
requestApplyPack(m_packPath, m_gameDir, m_packRemove);
|
||||
}
|
||||
|
||||
void BuildSwitcher::finishApplying()
|
||||
{
|
||||
// Пак сделал своё дело: держать сотни мегабайт рядом с архивом сборки не
|
||||
// за чем, а его наличие означало бы недоигранную раскатку.
|
||||
QFile::remove(m_packPath);
|
||||
|
||||
QJsonObject index = readIndex();
|
||||
index.insert(QStringLiteral("state"), kStateIdle);
|
||||
index.insert(QStringLiteral("active"), m_toId);
|
||||
index.remove(QStringLiteral("from"));
|
||||
index.remove(QStringLiteral("to"));
|
||||
index.remove(QStringLiteral("pack"));
|
||||
index.remove(QStringLiteral("packName"));
|
||||
index.remove(QStringLiteral("packRemove"));
|
||||
index.remove(QStringLiteral("packNote"));
|
||||
writeIndex(index);
|
||||
|
||||
m_lastPackNote = m_packNote;
|
||||
m_packPath.clear();
|
||||
m_packRemove.clear();
|
||||
m_packNote = {};
|
||||
|
||||
const int toId = m_toId;
|
||||
m_state = Idle;
|
||||
m_stage.clear();
|
||||
m_status.clear();
|
||||
m_done = m_total = 0;
|
||||
emit progressChanged();
|
||||
emit finished(toId);
|
||||
}
|
||||
|
||||
void BuildSwitcher::finish()
|
||||
{
|
||||
QJsonObject index = readIndex();
|
||||
@@ -379,7 +503,14 @@ bool BuildSwitcher::forgetBuild(int buildId)
|
||||
QString BuildSwitcher::interruptedSwitchWarning() const
|
||||
{
|
||||
const QJsonObject index = readIndex();
|
||||
if (index.value(QStringLiteral("state")).toString() != kStateSwitching)
|
||||
const QString state = index.value(QStringLiteral("state")).toString();
|
||||
|
||||
if (state == kStateApplying) {
|
||||
const QString name = index.value(QStringLiteral("packName")).toString();
|
||||
return tr("Установка сборки%1 не завершилась — докатываем её файлы.")
|
||||
.arg(name.isEmpty() ? QString() : tr(" «%1»").arg(name));
|
||||
}
|
||||
if (state != kStateSwitching)
|
||||
return {};
|
||||
|
||||
const int toId = index.value(QStringLiteral("to")).toInt(-1);
|
||||
@@ -395,7 +526,13 @@ QString BuildSwitcher::interruptedSwitchWarning() const
|
||||
void BuildSwitcher::resumeInterrupted(const QString &gameDir)
|
||||
{
|
||||
const QJsonObject index = readIndex();
|
||||
if (index.value(QStringLiteral("state")).toString() != kStateSwitching)
|
||||
const QString state = index.value(QStringLiteral("state")).toString();
|
||||
|
||||
if (state == kStateApplying) {
|
||||
resumeApplying(index, gameDir);
|
||||
return;
|
||||
}
|
||||
if (state != kStateSwitching)
|
||||
return;
|
||||
|
||||
const int toId = index.value(QStringLiteral("to")).toInt(-1);
|
||||
@@ -419,6 +556,50 @@ void BuildSwitcher::resumeInterrupted(const QString &gameDir)
|
||||
beginClearing();
|
||||
}
|
||||
|
||||
void BuildSwitcher::resumeApplying(const QJsonObject &index, const QString &gameDir)
|
||||
{
|
||||
const int toId = index.value(QStringLiteral("to")).toInt(-1);
|
||||
if (toId < 0 || m_state != Idle)
|
||||
return;
|
||||
|
||||
const QString packName = index.value(QStringLiteral("pack")).toString();
|
||||
const QString packPath = buildDir(toId) + u'/' + packName;
|
||||
if (packName.isEmpty() || packName.contains(u'/') || !QFile::exists(packPath)) {
|
||||
// Пак не сохранился — доигрывать нечем. Отметку снимаем, иначе
|
||||
// предупреждение о незавершённой установке будет всплывать вечно;
|
||||
// сборка при этом остаётся наполовину обновлённой, и лаунчер покажет
|
||||
// её как требующую обновления.
|
||||
QJsonObject cleaned = index;
|
||||
cleaned.insert(QStringLiteral("state"), kStateIdle);
|
||||
cleaned.remove(QStringLiteral("pack"));
|
||||
cleaned.remove(QStringLiteral("packName"));
|
||||
cleaned.remove(QStringLiteral("packRemove"));
|
||||
cleaned.remove(QStringLiteral("packNote"));
|
||||
cleaned.remove(QStringLiteral("to"));
|
||||
writeIndex(cleaned);
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList remove;
|
||||
const QJsonArray removeArray = index.value(QStringLiteral("packRemove")).toArray();
|
||||
for (const QJsonValue &value : removeArray)
|
||||
remove << value.toString();
|
||||
|
||||
m_fromId = -1;
|
||||
m_toId = toId;
|
||||
m_toName = index.value(QStringLiteral("packName")).toString();
|
||||
m_gameDir = gameDir;
|
||||
m_packPath = packPath;
|
||||
m_packRemove = remove;
|
||||
m_packNote = index.value(QStringLiteral("packNote")).toObject();
|
||||
m_lastPackEntries.clear();
|
||||
m_lastPackNote = {};
|
||||
m_done = m_total = 0;
|
||||
m_worker->clearCancel();
|
||||
m_state = Applying;
|
||||
beginApplying();
|
||||
}
|
||||
|
||||
// ── Реестр архивов ─────────────────────────────────────────────────────────
|
||||
|
||||
QJsonObject BuildSwitcher::readIndex() const
|
||||
@@ -445,7 +626,8 @@ void BuildSwitcher::writeIndex(const QJsonObject &index) const
|
||||
}
|
||||
|
||||
// Оборванная запись архива оставляет .tmp-*.zip: он никому не нужен и может
|
||||
// занимать гигабайты.
|
||||
// занимать гигабайты. Пакеты .pack-*.zip не трогаем: по ним доигрывается
|
||||
// прерванная раскатка сезонной сборки, а лишний тут удаляет resumeApplying.
|
||||
void BuildSwitcher::sweepTempArchives() const
|
||||
{
|
||||
QDir root(LauncherPaths::buildStorageDir());
|
||||
|
||||
Reference in New Issue
Block a user