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 ¬e, 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);
}
При создании этого документа использовался ИИ.