106 lines
6.5 KiB
Markdown
106 lines
6.5 KiB
Markdown
# BuildSwitcher
|
||
|
||
`buildswitcher.h`, `buildswitcher.cpp`
|
||
|
||
Смена активной сборки и раскатка пака сезонной сборки. Файлы двигает
|
||
[BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке, а этот класс решает, в каком порядке
|
||
это делать, и ведёт журнал, по которому можно продолжить после падения.
|
||
|
||
Экземпляр держит [LauncherBackend](LauncherBackend.md).
|
||
|
||
## Как устроены сборки на диске
|
||
|
||
В `.minecraft` в каждый момент разложена одна сборка, активная. Содержимое остальных лежит в
|
||
архивах `<root>/builds/<id>/<дата>_<имя>.zip`, по одному на сборку.
|
||
|
||
Общие для всех сборок папки и файлы никогда не архивируются и не удаляются (`sharedTopLevel()`):
|
||
|
||
- `assets`
|
||
- `libraries`
|
||
- `versions`
|
||
- `launcher_profiles.json`
|
||
- `runtime`
|
||
|
||
Всё остальное (моды, конфиги, миры, скриншоты, `options.txt`) принадлежит сборке.
|
||
|
||
В папке сборки могут лежать и другие zip, но файлы с точкой в начале имени не считаются архивом
|
||
сборки. Это временные архивы (`.tmp-*.zip`) и скачанные паки сезонных сборок (`.pack-r*.zip`).
|
||
|
||
## switchTo(fromId, fromName, toId, toName, gameDir)
|
||
|
||
1. **Архивация.** Если прежней сборки нет (`fromId < 0`) или папки игры нет, шаг пропускается.
|
||
Иначе сначала проверяется свободное место: размер всего, что уйдёт в архив, плюс 256 МБ. Потом
|
||
архив пишется во временный `builds/<from>/.tmp-<uuid>.zip`. Если на этом шаге что-то пошло не
|
||
так, `.minecraft` не тронута.
|
||
2. **Фиксация архива.** Временный файл переименовывается в `<гггг-мм-дд>_<имя>.zip`, остальные
|
||
архивы этой сборки удаляются. В `index.json` записывается `state: "switching"` с `from` и `to`.
|
||
3. **Очистка.** Из `.minecraft` удаляется всё, кроме общего.
|
||
4. **Распаковка.** Если у новой сборки есть архив, он распаковывается. Если нет (новая сборка),
|
||
`.minecraft` остаётся пустой, только с общими папками.
|
||
5. В `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`:
|
||
|
||
```json
|
||
{
|
||
"state": "idle",
|
||
"active": 3,
|
||
"builds": {
|
||
"2": { "archive": "2026-10-09_Vanilla.zip", "name": "Vanilla", "savedAt": "..." }
|
||
}
|
||
}
|
||
```
|
||
|
||
Во время работы к нему добавляются `from`, `to`, а для раскатки ещё `pack`, `packName`,
|
||
`packRemove`, `packNote`. Пишется через `QSaveFile`.
|
||
|
||
Деструктор просит работника остановиться и ждёт поток до 5 секунд.
|
||
|
||
---
|
||
|
||
При создании этого документа использовался ИИ.
|