# Целевая архитектура 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`, 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>` → `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`.