# BuildSwitcher ## Обзор класса `BuildSwitcher` меняет активную сборку: содержимое `.minecraft` уезжает в архив своей сборки, папка чистится, на её место разворачивается архив выбранной. Он же докатывает пак сезонной сборки поверх уже разложенного содержимого. Порядок шагов подчинён одному правилу: пока новый архив не записан целиком и не переименован на место, из `.minecraft` не удаляется ничего. Отметка о начатом переключении пишется в `builds/index.json` до первого разрушающего действия, поэтому обрыв питания или принудительное завершение процесса всегда обнаружим на следующем запуске. Сами файловые операции выполняет [BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке; `BuildSwitcher` — это порядок шагов, учёт состояния и восстановление после сбоя. ## Место в проекте и зависимости Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Класс создаёт и целиком владеет своим `QThread` и объектом [BuildArchiveWorker](BuildArchiveWorker.md). Пути к папкам сборок и файлу состояния даёт [launcherpaths.h](launcherpaths.md) (`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](BuildArchiveWorker.md), общение с которым идёт исключительно через очередь сигналов — прямых вызовов слотов исполнителя нет. Единственный межпотоковый примитив — атомарный признак отмены внутри исполнителя. ## Взаимодействие с другими классами `LauncherBackend` вызывает `switchTo()` при смене активной сборки, `applyPack()` — при установке или обновлении сезонной сборки, `forgetBuild()` — при удалении сборки. Сигналы переключателя он переправляет в свойства, которые читает QML: панель прогресса главного окна показывает `stage()`, `status()` и `fraction()`, а признак переключения блокирует список сборок и кнопки в [BuildsDialog](../qml/BuildsDialog.md). При старте бэкенд спрашивает `interruptedSwitchWarning()` и, если операция была прервана, предлагает доиграть её через `resumeInterrupted()`. Архив сезонной сборки к моменту вызова `applyPack()` уже скачан [SeasonalPackDownloader](SeasonalPackDownloader.md). ## Внешнее взаимодействие **Файловая система.** Класс читает и пишет `builds/index.json` — отметку о состоянии переключения, — управляет папками архивов сборок и подчищает временные файлы. Все операции с содержимым самой папки игры делегированы исполнителю в рабочем потоке. Сети и дочерних процессов у класса нет. ## Пример использования ```cpp 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); } ``` --- При создании этого документа использовался ИИ.