1 parent
1943996608
commit
f10b654953
34 files changed
+2099
-4576
No files matched your search
+72
-177
@@ -1,210 +1,105 @@
|
||||
# BuildSwitcher
|
||||
|
||||
## Обзор класса
|
||||
`buildswitcher.h`, `buildswitcher.cpp`
|
||||
|
||||
`BuildSwitcher` меняет активную сборку: содержимое `.minecraft` уезжает в архив своей сборки,
|
||||
папка чистится, на её место разворачивается архив выбранной. Он же докатывает пак сезонной
|
||||
сборки поверх уже разложенного содержимого.
|
||||
Смена активной сборки и раскатка пака сезонной сборки. Файлы двигает
|
||||
[BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке, а этот класс решает, в каком порядке
|
||||
это делать, и ведёт журнал, по которому можно продолжить после падения.
|
||||
|
||||
Порядок шагов подчинён одному правилу: пока новый архив не записан целиком и не переименован на
|
||||
место, из `.minecraft` не удаляется ничего. Отметка о начатом переключении пишется в
|
||||
`builds/index.json` до первого разрушающего действия, поэтому обрыв питания или принудительное
|
||||
завершение процесса всегда обнаружим на следующем запуске.
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md).
|
||||
|
||||
Сами файловые операции выполняет [BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке;
|
||||
`BuildSwitcher` — это порядок шагов, учёт состояния и восстановление после сбоя.
|
||||
## Как устроены сборки на диске
|
||||
|
||||
## Место в проекте и зависимости
|
||||
В `.minecraft` в каждый момент разложена одна сборка, активная. Содержимое остальных лежит в
|
||||
архивах `<root>/builds/<id>/<дата>_<имя>.zip`, по одному на сборку.
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Класс создаёт и целиком
|
||||
владеет своим `QThread` и объектом [BuildArchiveWorker](BuildArchiveWorker.md).
|
||||
Общие для всех сборок папки и файлы никогда не архивируются и не удаляются (`sharedTopLevel()`):
|
||||
|
||||
Пути к папкам сборок и файлу состояния даёт [launcherpaths.h](launcherpaths.md)
|
||||
(`buildStorageDir()`, `buildDir()`).
|
||||
- `assets`
|
||||
- `libraries`
|
||||
- `versions`
|
||||
- `launcher_profiles.json`
|
||||
- `runtime`
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QThread`, `QJsonObject`) и `Qt6::CorePrivate` — опосредованно,
|
||||
через исполнителя.
|
||||
Всё остальное (моды, конфиги, миры, скриншоты, `options.txt`) принадлежит сборке.
|
||||
|
||||
## Иерархия и роль
|
||||
В папке сборки могут лежать и другие zip, но файлы с точкой в начале имени не считаются архивом
|
||||
сборки. Это временные архивы (`.tmp-*.zip`) и скачанные паки сезонных сборок (`.pack-r*.zip`).
|
||||
|
||||
Наследует `QObject`: мета-объектная система, три сигнала и владение по родителю. Объявлен
|
||||
виртуальный деструктор — класс обязан корректно остановить свой поток.
|
||||
## 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)`.
|
||||
|
||||
#### explicit BuildSwitcher(QObject \*parent = nullptr)
|
||||
При ошибке приходит `failed(toId, message, gameDirIntact)`. `gameDirIntact` равен `true`, если
|
||||
ошибка случилась до очистки. Бэкенд по нему выбирает текст: «ничего не изменилось» или «папка
|
||||
игры разобрана».
|
||||
|
||||
Создаёт переключатель, рабочий поток и исполнителя, переносит второй в первый и запускает поток.
|
||||
Конструктор помечен `explicit`.
|
||||
`cancel()` работает только на шаге архивации. После него отступать уже некуда.
|
||||
|
||||
#### bool isRunning() const
|
||||
## applyPack(buildId, buildName, packZip, removeRelative, note, gameDir)
|
||||
|
||||
Идёт ли сейчас переключение или раскатка пака.
|
||||
Раскатка пака поверх активной сборки. Сначала в `index.json` пишется `state: "applying"` со всем,
|
||||
что нужно для повтора: имя файла пака, имя сборки, список на удаление и `note`. Потом работник
|
||||
удаляет файлы прошлой ревизии и распаковывает пак.
|
||||
|
||||
#### QString stage() const
|
||||
После успеха пак удаляется, `index.json` возвращается в `idle`, а `lastPackEntries()` и
|
||||
`lastPackNote()` содержат список разложенных файлов и `note`. Бэкенд забирает их в обработчике
|
||||
`finished` и пишет `season.json`.
|
||||
|
||||
Текущий этап словами — заголовок панели прогресса.
|
||||
`gameDirIntact` при ошибке раскатки равен `true`, только если работник не успел обработать ни одного
|
||||
файла.
|
||||
|
||||
#### QString status() const
|
||||
## После падения
|
||||
|
||||
Строка состояния: что обрабатывается сейчас.
|
||||
При старте бэкенд вызывает `interruptedSwitchWarning()`. Если в `index.json` осталось
|
||||
`switching` или `applying`, метод возвращает текст для пользователя, а `resumeInterrupted(gameDir)`
|
||||
продолжает операцию:
|
||||
|
||||
#### double fraction() const
|
||||
- `switching` — архив прежней сборки уже записан целиком, поэтому начинаем с очистки и
|
||||
распаковываем сборку `to`. Начинать с архивации нельзя: заархивировалась бы полуразобранная
|
||||
`.minecraft`;
|
||||
- `applying` — раскатка повторяется с тем же паком и тем же списком на удаление. Если файла пака
|
||||
уже нет, журнал просто сбрасывается в `idle`.
|
||||
|
||||
Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен.
|
||||
Если программа упала во время самой архивации, в журнале ещё ничего нет, и продолжать нечего:
|
||||
`.minecraft` цела, а временный архив удалится при следующем старте (`sweepTempArchives()` в
|
||||
конструкторе).
|
||||
|
||||
#### static QStringList sharedTopLevel()
|
||||
## Прочее
|
||||
|
||||
Общие для всех сборок каталоги верхнего уровня. Они не архивируются и не чистятся: принадлежат
|
||||
лаунчеру, а не какой-либо сборке. Список передаётся исполнителю как `excludeTop` при упаковке и
|
||||
как `keepTop` при очистке.
|
||||
- `forgetBuild(id)` — удаляет запись о сборке из `index.json` и папку `builds/<id>` целиком.
|
||||
- `hasArchive(id)` — есть ли у сборки архив.
|
||||
- `isRunning()`, `stage()`, `status()` (текущий файл), `fraction()` — для панели прогресса.
|
||||
Сигнал `progressChanged()` не ограничен по частоте и приходит на каждый обработанный файл.
|
||||
|
||||
#### void switchTo(int fromBuildId, const QString &fromName, int toBuildId, const QString &toName, const QString &gameDir)
|
||||
### index.json
|
||||
|
||||
Переключает активную сборку. Проходит четыре этапа: упаковка текущего содержимого во временный
|
||||
файл, фиксация архива переименованием на место, очистка папки игры и распаковка архива целевой
|
||||
сборки.
|
||||
`<root>/builds/index.json`:
|
||||
|
||||
Имена сборок нужны для текста в панели прогресса, идентификаторы — для путей к архивам.
|
||||
|
||||
#### 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);
|
||||
```json
|
||||
{
|
||||
"state": "idle",
|
||||
"active": 3,
|
||||
"builds": {
|
||||
"2": { "archive": "2026-10-09_Vanilla.zip", "name": "Vanilla", "savedAt": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Во время работы к нему добавляются `from`, `to`, а для раскатки ещё `pack`, `packName`,
|
||||
`packRemove`, `packNote`. Пишется через `QSaveFile`.
|
||||
|
||||
Деструктор просит работника остановиться и ждёт поток до 5 секунд.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
Reference in new issue
Block a user