6.5 KiB
BuildSwitcher
buildswitcher.h, buildswitcher.cpp
Смена активной сборки и раскатка пака сезонной сборки. Файлы двигает BuildArchiveWorker в отдельном потоке, а этот класс решает, в каком порядке это делать, и ведёт журнал, по которому можно продолжить после падения.
Экземпляр держит LauncherBackend.
Как устроены сборки на диске
В .minecraft в каждый момент разложена одна сборка, активная. Содержимое остальных лежит в
архивах <root>/builds/<id>/<дата>_<имя>.zip, по одному на сборку.
Общие для всех сборок папки и файлы никогда не архивируются и не удаляются (sharedTopLevel()):
assetslibrariesversionslauncher_profiles.jsonruntime
Всё остальное (моды, конфиги, миры, скриншоты, options.txt) принадлежит сборке.
В папке сборки могут лежать и другие zip, но файлы с точкой в начале имени не считаются архивом
сборки. Это временные архивы (.tmp-*.zip) и скачанные паки сезонных сборок (.pack-r*.zip).
switchTo(fromId, fromName, toId, toName, gameDir)
- Архивация. Если прежней сборки нет (
fromId < 0) или папки игры нет, шаг пропускается. Иначе сначала проверяется свободное место: размер всего, что уйдёт в архив, плюс 256 МБ. Потом архив пишется во временныйbuilds/<from>/.tmp-<uuid>.zip. Если на этом шаге что-то пошло не так,.minecraftне тронута. - Фиксация архива. Временный файл переименовывается в
<гггг-мм-дд>_<имя>.zip, остальные архивы этой сборки удаляются. Вindex.jsonзаписываетсяstate: "switching"сfromиto. - Очистка. Из
.minecraftудаляется всё, кроме общего. - Распаковка. Если у новой сборки есть архив, он распаковывается. Если нет (новая сборка),
.minecraftостаётся пустой, только с общими папками. - В
index.jsonзаписываютсяstate: "idle"иactive, испускаетсяfinished(toId).
При ошибке приходит failed(toId, message, gameDirIntact). gameDirIntact равен true, если
ошибка случилась до очистки. Бэкенд по нему выбирает текст: «ничего не изменилось» или «папка
игры разобрана».
cancel() работает только на шаге архивации. После него отступать уже некуда.
applyPack(buildId, buildName, packZip, removeRelative, note, gameDir)
Раскатка пака поверх активной сборки. Сначала в index.json пишется state: "applying" со всем,
что нужно для повтора: имя файла пака, имя сборки, список на удаление и note. Потом работник
удаляет файлы прошлой ревизии и распаковывает пак.
После успеха пак удаляется, index.json возвращается в idle, а lastPackEntries() и
lastPackNote() содержат список разложенных файлов и note. Бэкенд забирает их в обработчике
finished и пишет season.json.
gameDirIntact при ошибке раскатки равен true, только если работник не успел обработать ни одного
файла.
После падения
При старте бэкенд вызывает interruptedSwitchWarning(). Если в index.json осталось
switching или applying, метод возвращает текст для пользователя, а resumeInterrupted(gameDir)
продолжает операцию:
switching— архив прежней сборки уже записан целиком, поэтому начинаем с очистки и распаковываем сборкуto. Начинать с архивации нельзя: заархивировалась бы полуразобранная.minecraft;applying— раскатка повторяется с тем же паком и тем же списком на удаление. Если файла пака уже нет, журнал просто сбрасывается вidle.
Если программа упала во время самой архивации, в журнале ещё ничего нет, и продолжать нечего:
.minecraft цела, а временный архив удалится при следующем старте (sweepTempArchives() в
конструкторе).
Прочее
forgetBuild(id)— удаляет запись о сборке изindex.jsonи папкуbuilds/<id>целиком.hasArchive(id)— есть ли у сборки архив.isRunning(),stage(),status()(текущий файл),fraction()— для панели прогресса. СигналprogressChanged()не ограничен по частоте и приходит на каждый обработанный файл.
index.json
<root>/builds/index.json:
{
"state": "idle",
"active": 3,
"builds": {
"2": { "archive": "2026-10-09_Vanilla.zip", "name": "Vanilla", "savedAt": "..." }
}
}
Во время работы к нему добавляются from, to, а для раскатки ещё pack, packName,
packRemove, packNote. Пишется через QSaveFile.
Деструктор просит работника остановиться и ждёт поток до 5 секунд.
При создании этого документа использовался ИИ.