refactor: decompose god-classes, add lint gates, restructure packages

- Decompose all 11 god-classes (>200 lines) into facade + collaborators
  (command/, schematic/, config/, generator/, mode/, papi/, adaptive/,
  storage/, player/, leaderboard/) — every file is now <=200 lines
- Add PMD 7 (report-only) + stricter checkstyle rules (NPath, IllegalCatch,
  NestedDepth, Whitespace, MissingOverride); fix real bugs found
  (printStackTrace, resource leaks in ConfigLoader/SQLStatsStorage)
- docs/ARCHITECTURE.md: target structure + 'one folder = one purpose' rule
- Pilot restructure: command/core|util/, mode/impl/solo|multi|barrier/,
  hook/papi/resolver/; cross-package classes made public
- TODO.md: track warning cleanup (132-138) + restructure progress
This commit is contained in:
loki5512344 2026-08-05 18:33:21 +02:00
parent 1d57972038
commit 81788e71ed
Signed by: boba
GPG key ID: 253067914055423B
70 changed files with 3428 additions and 1701 deletions

250
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,250 @@
# Целевая архитектура LoParkour
> Руководство для последующих рефакторингов god-классов и добавления новых режимов (Duels, Elytra).
> Документ описывает **целевое** состояние; текущее зафиксировано в разделе «Текущее состояние».
---
## Принципы проектирования
1. **KISS** — минимум слоёв и абстракций для решения задачи; каждый класс делает одну вещь.
2. **DRY** — одна реализация фичи в одном месте; дублирование выносится в общий переиспользуемый слой
(напр. рендеринг/размещение блоков — в `block/`).
3. **SOLID**:
- **SRP** — у класса ровно одна ответственность (главный драйвер декомпозиции god-классов);
- **OCP** — новые режимы добавляются новыми подклассами/реализациями, без правки существующих;
- **ISP** — узкие интерфейсы (`Mode`, `MultiMode`, `GeneratorEventListener`);
- **DIP** — домен зависит от абстракций, инфраструктура зависит от домена.
4. **Композиция вместо наследования** — god-классы разбиваются на коллабораторов, инжектируемых в тонкий
фасад (паттерн уже применён в `ParkourGenerator`, `BlockPlacer`, `StorageSQL`, `Leaderboard` — развиваем его).
5. **Границы размера** (жёсткие, зафиксированы в `config/checkstyle/checkstyle.xml`, severity=`error`):
- файл ≤ **200 строк** (`FileLength`), метод ≤ **80 строк** (`MethodLength`),
параметров ≤ **7** (`ParameterNumber`), линия ≤ **150 символов** (`LineLength`),
`NPathComplexity` ≤ **50**, цикломатическая сложность ≤ **20**, вложенность if/for/try ≤ 3/2/2.
6. **Явный порядок зависимостей** — слои смотрят «вниз», циклы запрещены:
`api/` → `core/` (домен) → `block/` + subsystems (`generator/`, `schematic/`, `adaptive/`, `ghost/`,
`style/`, `world/`, `leaderboard/`, `reward/`, `duels/`, `elytra/`) → платформа
(`storage/`, `config/`, `hook/`, `listener/`, `command/`, `menu/`, `util/`).
7. **Новые файлы обязаны соответствовать всем правилам** — их нельзя добавлять в `suppressions.xml`.
Супрессии — только временный долг существующих legacy-файлов, удаляются по мере рефакторинга.
8. **Одна папка = одна цель.** Каждый пакет содержит файлы ОДНОЙ ответственности (пример нормы:
`command/schematic/` — все 6 файлов про схематик-команды). Если в папку попадают разные по смыслу
классы — режем на подпапки по под-целям (пример: `util/gui` — это часть подсистемы меню, а не общий
util; `mode/impl/` — соли-режимы, мульти-режимы и барьеры — разные цели). Ориентир: ~3-5 файлов на
папку; папка из 1 файла допустима, если это самостоятельная единица (util, model, msg), но НЕ ради
искусственной нарезки.
## Текущее состояние (кратко)
- **173 Java-файла** в `src/main/java/dev/loki/loparkour/` (база пакета `dev.loki.loparkour`), Java 21,
Paper 1.20.4 / Spigot API + Adventure, сборка shadow + checkstyle + PMD.
- Декомпозиция по паттерну «фасад + коллабораторы» уже частично проведена (`ParkourGenerator`,
`BlockPlacer`, `StorageSQL`, `Leaderboard`, `SessionStateManager`), но фасады и часть режимов всё ещё
превышают лимит 200 строк.
**God-классы (>200 строк) и где они «протекают»:**
| Класс | Строк | Протекающие обязанности |
|---|---|---|
| `command/LoParkourCommand` | 302 | роутинг по аргументам (0..4) + join/leaderboard логика + весь tab-complete |
| `command/schematic/SchematicCommandHandler` | 274 | сабкоманды схем + per-player кэш `SELECTIONS` |
| `config/core/ConfigUpdater` | 229 | шаблон-merge, YAML-парсинг full-path, бэкап/восстановление |
| `generator/jump/placement/BlockPlacer` | 227 | выбор BlockData, расчёт прыжка, запись блоков, эффекты, паста схем |
| `mode/impl/InvisibleBarrierMode` | 222 | `Mode`-фасад + `BarrierGenerator` + отрисовка контуров частицами |
| `hook/papi/PAPIHook` | 220 | метаданные expansion + роутинг + форматтеры рангов/сложности |
| `command/admin/AdminCommandHandler` | 214 | forcejoin/forceleave/reset/recoverinventory + резолв игроков |
| `adaptive/core/MetricsCollector` | 214 | метрики, near-miss-анализ, автосейв, кэш игроков |
| `storage/sql/StorageSQL` | 209 | инициализация пула, DDL, read/write скоров, read/write игрока |
| `player/core/ParkourPlayer` | 207 | Expose-настройки, скоринг-сет, статические реестры, сохранение, setup/hotbar |
| `leaderboard/core/Leaderboard` | 203 | CRUD, сортировка, тай-брейк, снапшоты рангов |
| `mode/impl/CoopMode` | 200 | на границе: `MultiMode` + `CoopGenerator` (contributions/милстоуны) |
Сопутствующие риски из `TODO.md`, влияющие на декомпозицию: статические mutable-поля
(`GeneratorState` — 15 публичных полей, `ParkourGenerator.session/island/profile/state/generatorOptions` —
п.107), рекурсия `unregister`/Coop (п.24), утечка SQL-пулов (п.27), per-player GUI-состояние (п.78),
неатомарный reload (п.70).
## Целевая структура пакетов
Новые режимы выносятся в **отдельные корневые пакеты** `duels/` и `elytra/` (не в существующий `mode/impl/`).
Общий переиспользуемый рендеринг/размещение блоков — в **`block/`**. Ядро домена — в **`core/`**.
Существующие пакеты сохраняются и встраиваются в слои.
```text
dev.loki.loparkour
│
├── api/ # Публичный контракт для внешних плагинов (единственный «экспортный» слой)
│ ├── core/ # ParkourAPI, Registry — регистрация режимов/стилей/событий
│ └── event/ # Доменные события (join/leave/score/fall/schematic/spectate/block-generate)
│
├── core/ # ЯДРО ДОМЕНА: агрегаты и доменные сервисы (слой ниже api/)
│ ├── model/ # Неизменяемые доменные модели (Score, Reward, GhostData и т.п.)
│ ├── player/ # Агрегат игрока: ParkourPlayer, ParkourUser, ParkourSpectator
│ ├── session/ # Session + жизненный цикл сессии (create/join/leave/removePlayers)
│ ├── mode/ # Контракты режимов: Mode, MultiMode, Modes
│ └── service/ # Доменные сервисы (UserRegistry, ParkourHotbar, ScoreboardManager)
│
├── block/ # ОБЩИЙ слой размещения/рендеринга блоков (для ВСЕХ генераторов и режимов)
│ ├── engine/ # BlockWriter: запись BlockData в мир + BlockEffectPlayer (particles/sound)
│ ├── selection/ # BlockSelector: выбор BlockData по стилям/весам (хук selectBlockData для режимов)
│ ├── style/ # BlockStyleProvider: материал/цвет/сдвиг блока по стилю
│ ├── jump/ # JumpCalculator, JumpValidator, JumpDirector, JumpOffsetGenerator, JumpType
│ ├── schematic/ # Паста схематик в мир (SchemPaster/StructurePaster, событие генерации)
│ ├── render/ # Частице-рендер: BlockOutlineRenderer, контуры/боксы (из InvisibleBarrierMode)
│ └── client/ # Клиент-сайд виртуальные блоки (без записи в мир) — использует Elytra
│
├── generator/ # SUBSYSTEM «бесконечная генерация» (сохраняется, использует block/)
│ ├── core/coordinator/ # ParkourGenerator (фасад), GeneratorProfileManager, GeneratorStatistics
│ ├── core/model/ # GeneratorState, Profile, Island, GeneratorOption
│ ├── jump/calculation/ # Расчёт прыжков (мигрирует в block/jump после стабилизации API)
│ ├── jump/placement/ # Оркестрация генерации блока (DEFAULT/SPECIAL/SCHEMATIC)
│ ├── lifecycle/loop/ # Тик/фолл/ресет/события генератора
│ ├── lifecycle/player/ # PlayerInteractionHandler, GeneratorCleanup
│ └── profile/ # GenerationProfileLoader (профили сложности)
│
├── schematic/ # SUBSYSTEM «схематики»: core/create/convert/nbt/schem/registry/legacy
├── adaptive/ # SUBSYSTEM «адаптивная сложность»: core, model, storage, bootstrap
├── ghost/ # SUBSYSTEM «ghost-реплей»: core (GhostManager/Recorder/Player), model
├── style/ # Визуальные темы: Style, RandomStyle
├── world/ # Зоны/острова: Divider, World
├── leaderboard/ # Лидерборды: core (Leaderboard/Sorter), model (Score), persistence
├── reward/ # Награды: Reward, Rewards
│
├── duels/ # НОВЫЙ РЕЖИМ «Duels» (корневой пакет, генератор на игрока)
│ ├── mode/ # DuelsMode implements MultiMode (create/join/leave, isAcceptingPlayers)
│ ├── generator/ # DuelsGenerator (owner) + SingleDuelsGenerator (генератор на игрока)
│ ├── match/ # goal, счёт, спавн-площадки, цвета игроков, initCountdown/win
│ └── menu/ # MultiplayerMenu / lobby invite (фильтр instanceof MultiMode)
│
├── elytra/ # НОВЫЙ РЕЖИМ «Elytra» (корневой пакет, клиент-сайд труба)
│ ├── mode/ # ElytraMode + вариации (Default/SpeedDemon/MinSpeed/TimeTrial/Close/Obstacle)
│ ├── section/ # ElytraSection, KnotDirector (сплайн/кольца), PointType
│ ├── render/ # Клиент-сайд рендер колец (sendBlockChanges)
│ └── generator/ # ElytraGenerator (свой tick()/fall-детект), фейерверк-буст
│
├── player/ # Существующий пакет (3-й слой, обслуживает core/player)
│ ├── core/ # ParkourPlayer, ParkourUser (агрегаты)
│ ├── data/ # PreviousData, InventoryData (save/restore состояния)
│ ├── service/ # ParkourHotbar, UserRegistry, ScoreboardManager, PlayerSettingsManager, BungeeUtil
│ └── spectator/ # ParkourSpectator
├── session/ # Session + Session*Manager (State/Player/User; тик пер-сессии)
├── mode/ # Существующие режимы: base/ (контракты), impl/solo|multi|barrier/
├── storage/ # Платформа хранения: Storage, disk/StorageDisk, sql/StorageSQL + SQL*
├── config/ # Платформа конфигурации: core, locale, options
├── hook/ # Платформенные интеграции: papi/ (+ resolver/), vault/, holo/, floodgate/
├── listener/ # Bukkit-листенеры: gameplay/, player/, schematic/
├── command/ # Команды: core/ (LoParkourCommand+Router+TabCompleter), util/, admin/, player/, schematic/
├── menu/ # GUI: core/, community/, lobby/, play/, settings/
└── util/ # Общие утилиты: gui/, item/, misc/, particle/, text/, world/
```
### Текущее состояние (после пилотной реструктуризации)
Правило «одна папка = одна цель» применено к 3 пилотным областям (остальные — в работе):
- **`command/`** — `core/` (LoParkourCommand, CommandRouter, CommandTabCompleter — вход/роутинг/табы),
`player/` (PlayerCommandHandler, JoinCommandExecutor, LeaderboardCommandExecutor),
`util/` (CommandUtil), `admin/`, `schematic/`.
- **`mode/impl/`** — `solo/` (DefaultMode, SpeedrunMode, GravityShiftMode), `multi/` (CoopMode, RaceMode,
SpectatorMode), `barrier/` (InvisibleBarrierMode, BarrierGenerator, BarrierRenderer).
- **`hook/papi/`** — `PAPIHook`+`PlaceholderDispatcher` (вход/диспатч), `resolver/` (Global/Player/ScoreRank
резолверы + PlaceholderFormatters).
Пересекающие пакеты классы стали `public` (JoinCommandExecutor, LeaderboardCommandExecutor, CommandUtil,
резолверы) — сигнатуры методов не менялись, только видимость.
## Карта миграции
Таблица «старый класс → решение» для god-классов. `split` — разделить на новых коллабораторов,
`keep` — оставить как есть, `move` — перенести ответственность в другой пакет без изменения поведения.
| Старый класс | Решение | Новые классы (пакет · ответственность) |
|---|---|---|
| `command/LoParkourCommand` | **split** | `command/CommandRouter` (диспетчер args.length 0..4) · `command/CommandContext` (sender/player/args-обёртка, сокращает параметры) · `command/JoinCommand` (join: режим/игрок/сессия) · `command/LeaderboardCommand` · `command/completion/CommandCompleter` (весь onTabComplete) · `command/completion/SchematicCompleter` + `ModeCompleter` (подкомандные) · `player/PlayerCommandHandler`, `admin/AdminCommandHandler`, `schematic/SchematicCommandHandler` — **keep** (уже выделены) |
| `command/schematic/SchematicCommandHandler` | **split** | `command/schematic/SchematicCommandRouter` (роутинг) · `.../SchematicCreateHandler` · `.../SchematicPasteHandler` · `.../SchematicWandSession` (per-player `SELECTIONS`, cleanup на quit — чинит п.84) · `.../SchematicConvertHandler` |
| `command/admin/AdminCommandHandler` | **split** | `command/admin/ForceJoinHandler` · `.../ForceLeaveHandler` · `.../ResetHandler` · `.../RecoverInventoryHandler` (по сабкоманде) · `command/admin/PlayerResolver` (resolveUUID/resolvePlayerName/findNearest; поддержка `BlockCommandSender` — п.119) |
| `config/core/ConfigUpdater` | **split** | `config/core/ConfigTemplate` (чтение шаблона) · `config/core/YamlPathParser` (full-path-парсинг, из `extractFullPathValues`) · `config/core/ConfigMerger` (точечный merge отсутствующих ключей, сохранение листов/комментариев — фикс п.26) · `config/core/ConfigBackup` (бэкап/восстановление) · `ConfigUpdater` = тонкий фасад (keep) |
| `generator/jump/placement/BlockPlacer` | **split+move** | `block/engine/BlockWriter` (placeBlockData, setBlockData+phys) · `block/engine/BlockEffectPlayer` (particles/sound на всех игроков) · `block/selection/BlockSelector` (**move**, уже есть) · `block/jump/JumpCalculator` (**move**, уже есть) · `generator/jump/placement/BlockGenerationOrchestrator` (generateSingleBlock, switch DEFAULT/SPECIAL/SCHEMATIC) · `generator/jump/placement/SchematicBlockGenerator` (tryGenerateSchematic) · `block/jump/TallMaterialResolver` (isTallMaterial + форм.высота) |
| `mode/impl/InvisibleBarrierMode` | **split+move** | `mode/impl/InvisibleBarrierMode` (keep, тонкий фасад) · `mode/impl/barrier/BarrierGenerator` (keep как есть, переезжает в подпакет) · `mode/impl/barrier/BarrierVisualState` (activeBarriers + blockColors + self-clean) · `block/render/BlockOutlineRenderer` (drawEdge/drawBlockOutline) · `block/render/ParticleBoxGeometry` (record Point + рёбра) |
| `hook/papi/PAPIHook` | **split** | `hook/papi/PAPIHook` (keep, тонкий: метаданные expansion + роутинг) · `hook/papi/PlaceholderRegistry` (плейсхолдер → resolver) · `hook/papi/PlayerPlaceholders` (score/time/lead/style/difficulty…) · `hook/papi/LeaderboardPlaceholders` (rank_*, leader/record) · `hook/papi/DifficultyFormatter` (parseDifficulty) · опционально `ScoreUntilPlaceholder` (score_until_N) · переименовать id `witp` → `loparkour` (п.118) |
| `storage/sql/StorageSQL` | **split** | `storage/sql/StorageSQL` (keep, фасад) · `storage/sql/SQLPool` (один Hikari-пул на плагин — фикс п.27, `setInitializationFailTimeout(-1)` п.28) · `storage/sql/SQLSchemaManager` (единый мигратор DDL — фикс п.62) · `storage/sql/SQLScoreRepository` (readScores/writeScores) · `storage/sql/SQLPlayerRepository` (readPlayer/writePlayer) · `storage/sql/SQLCallbackQueue` (runWhenConnected, фикс п.29) · `SQLQueryBuilder`, `SQLDataMapper`, `SQLMigrationManager` — keep |
| `player/core/ParkourPlayer` | **split** | `core/player/ParkourPlayer` (*keep* агрегат, тонкий) · `player/data/PlayerSettings` (Expose-поля настроек + их serialize/restore) · `player/service/ScoreTracker` (scoredBlocks/hasScored/markScored/blockKey) · `player/service/PlayerPersistence` (save/read) · `player/service/PlayerSetup` (setup/teleport/hotbar) · `player/service/GeneratorSettingsSync` (updateGeneratorSettings) · `player/core/ParkourPlayerRegistry` (статические getPlayer/getPlayers/isPlayer — заодно фикс O(n), п.47) |
| `leaderboard/core/Leaderboard` | **split** | `leaderboard/core/Leaderboard` (keep, фасад) · `leaderboard/core/ScoreComparator` (isBetterThan/тай-брейк) · `leaderboard/core/LeaderboardQueries` (getRank/getScoreAtRank/sort-снапшоты) · `LeaderboardStorage`, `LeaderboardSorter` — keep · **не split**: `model/Score.getTimeMillis` — bugfix парсинга `mm:ss.SSS`/`HH:mm:ss.SSS` и `long` (п.25) |
| `adaptive/core/MetricsCollector` | **split** | `adaptive/core/MetricsCollector` (keep: сценарии GeneratorEventListener) · `adaptive/core/JumpAnalyzer` (near-miss, время на блок) · `adaptive/core/MetricsAutoSave` (таймер автосейва, фикс Folia п.21) · `adaptive/core/PlayerMetricsCache` (кэш/выгрузка) · `adaptive/storage/StatsRepository` — keep |
| `mode/impl/CoopMode` | **split** (на границе 200) | `mode/impl/coop/CoopMode` (keep: MultiMode-фасад) · `mode/impl/coop/CoopSession` (создание лобби/инвайтов, создание сессии в create — фикс п.23) · `mode/impl/coop/CoopGenerator` (contributions/милстоны) · guard в unregister (фикс рекурсии п.24) |
## Новые режимы
Оба режима разрабатываются сразу в чистом виде (полное соответствие checkstyle, ноль PMD-нарушений) —
новая функциональность не может «перевозить» legacy-долг. Код режима НЕ добавляется в `suppressions.xml`.
### Duels — `duels/*` (в терминах задачи `mode/duels/*`)
Модель «генератор на игрока»: одна сессия-владелец, у каждого игрока свой генератор.
- **Пакеты:** `duels/mode/DuelsMode` (`implements MultiMode`), `duels/generator/DuelsGenerator`
(owner, `Map<ParkourPlayer, SingleDuelsGenerator>`, allowJoining, goal, spawnData),
`duels/generator/SingleDuelsGenerator` (`extends ParkourGenerator` со своим `state`/`history`/цветом/островом),
`duels/match/` (goal, счёт, initCountdown, win с guard `stopped`, идемпотентный синхронный `reset(false)`),
`duels/menu/MultiplayerMenu` + кнопка в `PlayMenu`.
- **Переиспользуют ядро:** `ParkourGenerator` (база), `BlockPlacer` + `block/*`, `PlayerInteractionHandler`
(скоринг по своей трассе), `ParkourHotbar` (баннер-старт), `PreviousData` (сейв/восстановление),
`Session`, `UserRegistry`. Результаты НЕ пишутся в лидерборд (`getLeaderboard() = null`).
- **Требования к ядру:** хук `selectBlockData()` (фиксированный цвет на игрока), пер-игроковая спавн-площадка
со сдвигом, ослабить `final class Island`, leave() без `ParkourUser.leave`-рекурсии (guard-флаг, п.24).
- **Не повторять баги Race/Coop:** без TIME-лидерборда (время брать своим форматтером из `state.start`),
ресет только синхронно с null-guard, сессия создаётся в `create()`, `join()` через `isAcceptingPlayers`.
### Elytra — `elytra/*` (в терминах задачи `mode/elytra/*`)
Один генератор на игрока, трасса = секции узлов → сплайн → «кольца»; **клиент-сайд рендер** без записи в мир.
- **Пакеты:** `elytra/section/ElytraSection`, `elytra/section/ElytraKnotDirector` (dx~N(75,15), dy-bias, dz~N(0,35)),
`elytra/section/ElytraPointType` (CIRCLE/FLAT), `elytra/generator/ElytraGenerator` (`extends ParkourGenerator`,
свой `tick()`, переопределённый `getMode()`, свой fall-детект — не полагаться на `LifecycleTickManager.checkPlayerFall`),
`elytra/mode/` (ElytraMode + Default/SpeedDemon/MinSpeed/TimeTrial/Close/Obstacle — по подклассу на режим),
`elytra/render/` (очередь `Map<chunkX, Set<Vector>>` → `player.sendBlockChanges(states)`, лимит min(viewDistance, 8)).
- **Переиспользуют ядро:** `ParkourGenerator` (база + профили сложности), `ParkourHotbar`, `PreviousData`,
`block/client/` (виртуальные блоки), зону `Divider`; скор — double (добавить float/double-вариант `Score` или
отдельный счётчик).
- **Требования к ядру:** пер-игроковый `state` (не разделяемый на сессию), высота/порог ASCEND под наши
zone-границы, раздача элитр в слот нагрудника через `ParkourHotbar`.
- **Зависимости:** Apache Commons Math (`SplineInterpolator`) или Catmull-Rom — без стороннего мира.
Тесты новых режимов (п.131): сплайн/секции, `ElytraGenerator.tick` (score по velocity.x, причины reset),
`DuelsGenerator.win` (guard/идемпотентность), `SingleDuelsGenerator.score→win` при goal, регрессия рекурсии unregister.
## Политика линтеров
- **Checkstyle — жёсткий гейт** (`isIgnoreFailures=false`, `maxErrors=0` в `build.gradle.kts`): правило `FileLength`
(200) и прочие жёсткие проверки падают сборку. Сейчас сборка проходит `0 errors`, но копит **~1429 warning**.
- **Шумные правила — severity=`warning`, не блокируют** (задано в `checkstyle.xml`): `MagicNumber`,
`FinalLocalVariable`, `EmptyLineSeparator`, `ClassDataAbstractionCoupling`, `ClassFanOutComplexity`,
`MissingDeprecated`. Они не требуют супрессий и чистятся отдельным проходом.
- **Legacy-нарушения — точечные супрессии** в `config/checkstyle/suppressions.xml` (`FileLength`,
`IllegalCatch`, `NPathComplexity`, `WhitespaceAround`). Каждая запись привязана к конкретному файлу,
удаляется сразу после декомпозиции/зачистки этого файла. **Новые файлы в супрессии не добавляются.**
- **PMD — report-only** (`isIgnoreFailures=true`), сейчас **45 нарушений**:
`CognitiveComplexity(17)` + `CyclomaticComplexity(7)` + `GodClass(4)` + `TooManyMethods(8)` — декомпозиция;
`AvoidReassigningParameters(7)` — переименование параметров; `EmptyCatchBlock(2)` — комментарий/тип.
После зачистки (п.137) `isIgnoreFailures` убирается, PMD становится гейтом (п.138).
- В `TODO.md` «Зачистка варнингов линтеров»: `FinalLocalVariable` ~1034 (чистить вместе с декомпозицией),
`MagicNumber` ~346, `WhitespaceAround` 30, `NPathComplexity` 29, `IllegalCatch` 22.
## Порядок работ
1. **Архитектура** — утвердить этот документ, зафиксировать целевое дерево пакетов и карту миграции.
2. **Декомпозиция god-классов** — по карте миграции (BlockPlacer → `block/*`, LoParkourCommand → роутеры,
PAPIHook → реестры плейсхолдеров, StorageSQL → репозитории + один пул, ParkourPlayer → сервисы,
InvisibleBarrierMode → `block/render`, ConfigUpdater → мержер и т.д.). После каждого файла — удаление его
супрессии из `suppressions.xml` (сборка обязана оставаться зелёной).
3. **Зачистка варнингов** — `FinalLocalVariable`/`MagicNumber`/`WhitespaceAround`/`NPath`/`IllegalCatch`
по отчётам `build/reports/checkstyle/`; закрыть оставшиеся PMD-нарушения.
4. **Elytra** — база + режимы + клиент-сайд рендер (меньше зависимостей, чем Duels; S/M-сложность).
5. **Duels** — `duels/*` с предварительными хуками ядра (`selectBlockData`, пер-игроковый state,
`final Island`); требует предварительно закрытого Coop-рекурсивного цикла (п.24).
6. **PMD-гейт** — убрать `isIgnoreFailures=true`, PMD в CI как жёсткий гейт; удалить пустые записи из
`suppressions.xml`.