Files
2026-09-03 09:16:56 +03:00

13 KiB

BuildSwitcher

Обзор класса

BuildSwitcher меняет активную сборку: содержимое .minecraft уезжает в архив своей сборки, папка чистится, на её место разворачивается архив выбранной. Он же докатывает пак сезонной сборки поверх уже разложенного содержимого.

Порядок шагов подчинён одному правилу: пока новый архив не записан целиком и не переименован на место, из .minecraft не удаляется ничего. Отметка о начатом переключении пишется в builds/index.json до первого разрушающего действия, поэтому обрыв питания или принудительное завершение процесса всегда обнаружим на следующем запуске.

Сами файловые операции выполняет BuildArchiveWorker в отдельном потоке; BuildSwitcher — это порядок шагов, учёт состояния и восстановление после сбоя.

Место в проекте и зависимости

Экземпляр создаётся и принадлежит LauncherBackend. Класс создаёт и целиком владеет своим QThread и объектом BuildArchiveWorker.

Пути к папкам сборок и файлу состояния даёт launcherpaths.h (buildStorageDir(), buildDir()).

Требования сборки: Qt6::Core (QThread, QJsonObject) и Qt6::CorePrivate — опосредованно, через исполнителя.

Иерархия и роль

Наследует QObject: мета-объектная система, три сигнала и владение по родителю. Объявлен виртуальный деструктор — класс обязан корректно остановить свой поток.

Публичные методы

explicit BuildSwitcher(QObject *parent = nullptr)

Создаёт переключатель, рабочий поток и исполнителя, переносит второй в первый и запускает поток. Конструктор помечен explicit.

bool isRunning() const

Идёт ли сейчас переключение или раскатка пака.

QString stage() const

Текущий этап словами — заголовок панели прогресса.

QString status() const

Строка состояния: что обрабатывается сейчас.

double fraction() const

Доля выполнения от 0 до 1 либо -1, пока итог неизвестен.

static QStringList sharedTopLevel()

Общие для всех сборок каталоги верхнего уровня. Они не архивируются и не чистятся: принадлежат лаунчеру, а не какой-либо сборке. Список передаётся исполнителю как excludeTop при упаковке и как keepTop при очистке.

void switchTo(int fromBuildId, const QString &fromName, int toBuildId, const QString &toName, const QString &gameDir)

Переключает активную сборку. Проходит четыре этапа: упаковка текущего содержимого во временный файл, фиксация архива переименованием на место, очистка папки игры и распаковка архива целевой сборки.

Имена сборок нужны для текста в панели прогресса, идентификаторы — для путей к архивам.

void applyPack(int buildId, const QString &buildName, const QString &packZipPath, const QStringList &removeRelative, const QJsonObject &note, const QString &gameDir)

Докатывает пак сезонной сборки поверх содержимого .minecraft. Вызывается только для уже активной сборки: пак ложится на то, что сейчас разложено, а не внутрь чужого архива.

Параметр removeRelative — файлы, ушедшие из сборки в новой ревизии; они удаляются перед распаковкой. Всего остального операция не касается, поэтому миры и скриншоты игрока переживают обновление.

Параметр note — непрозрачные данные вызывающей стороны. Они переживают перезапуск вместе с отметкой о незавершённой операции и возвращаются через lastPackNote(), когда раскатка доиграна. Сам переключатель в них не заглядывает.

QStringList lastPackEntries() const

Что принёс последний успешно раскатанный пак — относительными путями. Список нужен следующему обновлению, чтобы вычислить, какие файлы из сборки ушли.

QJsonObject lastPackNote() const

Данные, с которыми пришёл последний успешно раскатанный пак, — те самые, что передавались в applyPack().

void cancel()

Отменяет текущую операцию. Отмена доходит до исполнителя через признак, проверяемый между файлами.

Отменять имеет смысл только на этапе упаковки: после очистки .minecraft отступать некуда, и операцию нужно довести до конца. Именно поэтому панель прогресса смены сборки объявлена неотменяемой.

bool forgetBuild(int buildId)

Сборку удалили — убирает её архив и запись о нём. Возвращает false, если папку архива не удалось удалить целиком.

bool hasArchive(int buildId) const

Есть ли у сборки сохранённый архив. Используется в тексте предупреждения об удалении: вместе со сборкой пропадут её моды, конфиги и миры.

QString interruptedSwitchWarning() const

Незавершённое переключение с прошлого запуска. Возвращает пустую строку, если всё в порядке, иначе — готовый текст для пользователя.

Проверяется при старте лаунчера: отметка в builds/index.json пишется до первого разрушающего действия, поэтому прерванная операция обнаруживается всегда.

void resumeInterrupted(const QString &gameDir)

Доигрывает прерванное переключение: очистку и распаковку целевой сборки. Архив исходной сборки к этому моменту уже записан — правило порядка шагов это гарантирует.

Сигналы

progressChanged()

Изменились числа прогресса. Обработчик перечитывает stage(), status() и fraction().

finished(int toBuildId)

Переключение или раскатка завершены успешно; в параметре — идентификатор сборки, ставшей активной.

Обработчик снимает признак переключения, обновляет активную сборку и, после раскатки пака, забирает lastPackEntries() и lastPackNote(), чтобы записать их в описание сезонной сборки.

failed(int toBuildId, const QString &message, bool gameDirIntact)

Операция не удалась. Третий параметр — ключевой: он говорит, цела ли папка игры. Неудача на этапе упаковки оставляет .minecraft нетронутой, неудача после очистки — нет, и сообщение пользователю должно различать эти случаи.

Владение и время жизни

Класс наследует QObject и принимает parent — родитель его и удалит.

Рабочий поток и исполнитель создаются в конструкторе и принадлежат переключателю. Исполнитель родителя не имеет — объект, живущий в другом потоке, не может принадлежать объекту из потока GUI; его удаление привязано к завершению потока. Деструктор обязан остановить поток и дождаться его завершения, иначе рабочая операция переживёт своего владельца.

Временный архив на диске переживает аварийное завершение процесса; накопившиеся временные файлы подчищаются при следующем запуске.

Потокобезопасность

Сам переключатель живёт в потоке GUI: все его публичные методы вызываются оттуда. Тяжёлая работа вынесена в отдельный поток к BuildArchiveWorker, общение с которым идёт исключительно через очередь сигналов — прямых вызовов слотов исполнителя нет.

Единственный межпотоковый примитив — атомарный признак отмены внутри исполнителя.

Взаимодействие с другими классами

LauncherBackend вызывает switchTo() при смене активной сборки, applyPack() — при установке или обновлении сезонной сборки, forgetBuild() — при удалении сборки. Сигналы переключателя он переправляет в свойства, которые читает QML: панель прогресса главного окна показывает stage(), status() и fraction(), а признак переключения блокирует список сборок и кнопки в BuildsDialog.

При старте бэкенд спрашивает interruptedSwitchWarning() и, если операция была прервана, предлагает доиграть её через resumeInterrupted().

Архив сезонной сборки к моменту вызова applyPack() уже скачан SeasonalPackDownloader.

Внешнее взаимодействие

Файловая система. Класс читает и пишет builds/index.json — отметку о состоянии переключения, — управляет папками архивов сборок и подчищает временные файлы. Все операции с содержимым самой папки игры делегированы исполнителю в рабочем потоке.

Сети и дочерних процессов у класса нет.

Пример использования

auto *switcher = new BuildSwitcher(this);

connect(switcher, &BuildSwitcher::progressChanged, this, &Backend::switchChanged);
connect(switcher, &BuildSwitcher::finished, this, &Backend::onSwitchFinished);
connect(switcher, &BuildSwitcher::failed, this,
        [this](int, const QString &message, bool gameDirIntact) {
            emit launchError(gameDirIntact
                             ? message
                             : tr("%1. Папка игры осталась незавершённой.").arg(message));
        });

// при старте лаунчера
const QString warning = switcher->interruptedSwitchWarning();
if (!warning.isEmpty()) {
    showStatus(warning);
    switcher->resumeInterrupted(gameDir);
}

При создании этого документа использовался ИИ.