LoParkour/docs/ARCHITECTURE.md
loki5512344 9f70e2e586
refactor: remove legacy .lpschem format (TODO 117)
- Delete schematic/legacy/lpschem/ (LPSchematicLegacy + 5 pojo Gson models)
- Delete schematic/convert/LpschemConverter and /lp schematic convert command
- Remove SchematicClipboardBuilder.fromLegacy, convert branches in dispatcher/
  tab-completer/help, locales (ru/en), SCHEMATICS.md and yml comments
- Kept only .schem (WorldEdit) + .nbt
- Build green: checkstyle 0 errors, PMD 33
2026-08-05 18:58:18 +02:00

30 KiB
Raw Blame History

Целевая архитектура 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/. Существующие пакеты сохраняются и встраиваются в слои.

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/

Текущее состояние (после реструктуризации)

Правило «одна папка = одна цель» применено к переполненным пакетам:

  • command/ — core/ (LoParkourCommand, CommandRouter, CommandTabCompleter — вход/роутинг/табы), util/ (CommandUtil), player/ (PlayerCommandHandler, Join, Leaderboard), admin/ (ops/ — Force*/Reset/ RecoverInventory, support/ — TargetResolver, Responses), schematic/ (одна цель — схематик-команды).
  • mode/impl/ — solo/ (DefaultMode, SpeedrunMode, GravityShiftMode), multi/ (CoopMode, RaceMode, SpectatorMode), barrier/ (InvisibleBarrierMode, BarrierGenerator, BarrierRenderer).
  • hook/papi/ — PAPIHook+PlaceholderDispatcher (вход/диспатч), resolver/ (Global/Player/ScoreRank резолверы + PlaceholderFormatters).
  • storage/sql/ — корень (StorageSQL, SQLConnectionManager, SQLDataMapper), schema/ (SQLSchemaManager, SQLMigrationManager), query/ (SQLQueryBuilder, SQLQueryExecutor), repo/ (SQLPlayerRepository, SQLScoreRepository).
  • adaptive/core/ — корень (AdaptiveDifficulty, MetricsCollector), calc/ (DifficultyCalculator/ Adjuster/SkillAnalyzer), metrics/ (MetricEventCollector, MetricsCacheStore, JumpTimingTracker), model/ (DifficultyWeights).
  • api/event/ — session/ (Join/Leave/Spectate), score/ (Score/Fall), generation/ (Block/Schematic).
  • config/core/ — корень (Config, ConfigLoader, ConfigAccessor), merge/ (ConfigUpdater, ConfigMergeParser, ConfigMergeProcessor).
  • config/options/ — корень (Option, OptionGeneral), section/ (Generation/Particles/SQL/Styles).
  • player/service/ — корень (UserRegistry, PlayerSettingsManager, BungeeUtil), ui/ (ParkourHotbar, ScoreboardManager).
  • menu/core/ — корень (LPMenu, Menus, MenuStub), screen/ (MainMenu, DynamicMenu, ParkourOption); util/gui/ переехали в menu/gui/ (GUI migration-стабы).
  • schematic/legacy/lpschem/ — LPSchematicLegacy + pojo/ удалено (п.117 TODO): формат .lpschem выпилен, остались .schem (WorldEdit) + .nbt.

Пересекающие пакеты классы стали public (сигнатуры методов не менялись, только видимость). Оставшиеся папки с 4–6 файлами — цельные по смыслу (jump-расчёты, block-placement, player-core, pojo и т.п.) — не дробятся.

Карта миграции

Таблица «старый класс → решение» для 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 (dxN(75,15), dy-bias, dzN(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.