- ElytraGenerator no longer hardcodes Modes.elytra(): the mode is passed through the constructor (canonical (session, pointType, mode) plus two convenience overloads); CloseGenerator/MinSpeed/Obstacle/SpeedDemon/ TimeTrial now declare their own mode, so getMode() is reachable - Session.ChatType gets next(); ParkourUser gets setChatType(); SettingsMenu gains a chat-scope cycle button (slot 15) so LOBBY_ONLY/PLAYERS_ONLY are no longer dead branches - ChatTypeTest covers the cycle order and wrap-around - TODO.md: PROGRESS-8 records both fixes checkstyle 0 error / 135 warning, PMD 0, tests 96/0 fail (1 skipped)
648 lines
97 KiB
Markdown
648 lines
97 KiB
Markdown
# LoParkour Bug Tracker & Task List
|
||
|
||
## ✅ Исправлено (история, 19/20)
|
||
|
||
### CRITICAL (7/7 fixed)
|
||
|
||
- ~~**1. ProfileValue.asInt/asDouble — NumberFormatException при пустой строке**~~ ✅
|
||
- ~~**2. Bukkit.createBlockData — IllegalArgumentException при невалидном materialName**~~ ✅
|
||
- ~~**3. JumpDirector диагональный дрейф — heading никогда не выпрямляется**~~ ✅
|
||
- ~~**4. subList().clear() на synchronizedList(LinkedList) — повреждение node связей**~~ ✅
|
||
- ~~**5. Мультиплеер — runaway генерация блоков**~~ ✅
|
||
- ~~**6. Система схематик полностью сломана**~~ ✅
|
||
- ~~**7. Прыжки за границами валидации**~~ ✅
|
||
|
||
### HIGH (4/4 fixed)
|
||
|
||
- ~~**8. angleInY вращает offset относительно глобального Option.HEADING**~~ ✅
|
||
- ~~**9. Дублирование canJump(Location) и canJump(Vector) — рассинхронизация**~~ ✅
|
||
- ~~**10. synchronizedList — итерации без синхронизации**~~ ✅
|
||
- ~~**11. heading.clone() без null-проверки**~~ ✅
|
||
|
||
### MEDIUM (5/6 fixed)
|
||
|
||
- ~~**12. BlockSelector.selectBlockData рекурсия при пустом списке стилей**~~ ✅
|
||
- ~~**13. checkPlayerFall — break вместо continue**~~ ✅
|
||
- ~~**14. removeTrailBlocks сдвигает индексы истории**~~ ✅
|
||
- ~~**15. Дублирование isSlabMaterial**~~ ✅
|
||
- ~~**16. DifficultyScore thresholds для льда (1.0/2.0)**~~ ✅
|
||
- **17. schematicBlocks — ArrayList без thread safety** — main-thread-only, неактуально
|
||
|
||
### LOW (3/3 fixed)
|
||
|
||
- ~~**18. scoredBlocks HashSet не очищается при fall**~~ ✅
|
||
- ~~**19. getFormattedTime() — Date(elapsed) хак**~~ ✅
|
||
- ~~**20. isUnsafeParkourMaterial блокирует PUMPKIN и _BED**~~ ✅
|
||
|
||
---
|
||
|
||
## 🆕 Открыто после полного аудита (2026-08-05)
|
||
|
||
Пометки: `[P1]` критично, `[P2]` высоко, `[P3]` средне, `[P4]` низко.
|
||
|
||
---
|
||
|
||
## 🔴 [P1] Критическое — чинить в первую очередь
|
||
|
||
### Функциональность, которая не работает
|
||
|
||
- **21. Folia-поддержка — фикция.** Нет `folia-plugin.yml` (Folia не загрузит плагин). `MetricsCollector` использует удалённое API `BukkitRunnable.runTaskTimerAsynchronously` (MetricsCollector.java:73). `SessionStateManager.runTimer` → GlobalRegionScheduler, но тик мутирует блоки/entities (SessionStateManager.java:76, LifecycleTickManager.java:75, BlockPlacer.java:158, teleport в ParkourUser.java:93-95). → Либо выпилить бейдж Folia из README, либо: добавить folia-plugin.yml, перевести тик на runTimerAtLocation/runAtEntity, заменить BukkitRunnable.
|
||
- **22. Ghost-система — 100% мёртвый код.** `GhostManager`/`GhostRecorder`/`GhostPlayer` нигде не инстанцируются (grep: только само-ссылки). Конфиг `ghost-mode.*` не потребляется. → Подключить к lifecycle (запись в saveGhost/спавн в spawnGhosts) ИЛИ удалить подсистему и конфиг.
|
||
- **23. Coop-режим нерабочий.** Ни одно `Session.create` не строит `CoopGenerator` (CoopMode.java:134). `/parkour join <игрок>` ведёт в спектаторы. → Реализовать создание coop-сессии (лобби/инвайты) или выпилить режим из меню и регистрации.
|
||
- **24. Coop: латентный StackOverflow.** `ParkourPlayer.unregister()` → `CoopMode.leave()` → `ParkourUser.leave()` → `UserRegistry.unregister()` → `user.unregister()` — бесконечная рекурсия (ParkourPlayer.java:102-107, CoopMode.java:115-121, UserRegistry.java:58-67). Нет guard-флага. → Добавить in-flight guard в unregister (например, reentrancy flag в ParkourUser).
|
||
- **25. Лидерборд: тайминги сломаны.** `Score.getTimeMillis()` ждёт ровно 3 части через `:` (Score.java:36-55), но генератор пишет `mm:ss.SSS` (2 части) и `HH:mm:ss.SSS` (третья `02.123` → NFE). Итог: TIME-сортировка и тай-брейк мёртвые, Race-лидерборд не обновляется. Плюс переполнение `int` в m*60*1000+... (Score.java:51). → Унифицировать формат времени; парсить `mm:ss.SSS` и `HH:mm:ss.SSS`; перейти на `long`.
|
||
- **26. ConfigUpdater уничтожает пользовательские данные.** `merge()` перегенерирует весь файл из шаблона при каждом старте (ConfigUpdater.java:85-127): теряются листы (`options.leads.amount`, стили), кастомные ключи, комментарии; алгоритм отступов (`prevIndent -= 2`) ошибочен; `ignoredSections` не защищают. → Точечный merge только отсутствующих ключей, сохранять листы/комментарии, либо заменить на библиотеку (e.g. `me.mattstudios` / SimpleConfig) или `Bukkit's config addDefaults`.
|
||
- **27. SQL: утечка Hikari-пулов.** `StorageSQL.init(mode)` вызывается на каждый режим (6 лидербордов), каждый раз пересоздаёт `connectionManager` (StorageSQL.java:66-77, LeaderboardStorage.java:35) → до ~10 соединений висят навсегда. → Один пул на плагин, инициализация один раз.
|
||
- **28. SQL: блокирующий connect на main thread** (SQLConnectionManager.java:34-56) — при лежащей БД включение висит до 5с×режимы. → `setInitializationFailTimeout(-1)` + асинхронный connect.
|
||
- **29. SQL: адаптив не стартует.** `AdaptiveServices.init` вызывается до создания лидербордов, а `StorageSQL.runWhenConnected` выбрасывает колбэк при не-подключении (StorageSQL.java:45-51). → Очередь колбэков в SQLConnectionManager.onConnectCallbacks; порядок init: лидерборды → storage → adaptive.
|
||
- **30. SQL: настройки игрока не сохраняются.** ✅ **СДЕЛАНО**: `SQLDataMapper.serializeSettings` пишет `PlayerSettingsManager.toSettingsMap(player)` (12 ключей) в JSON-колонку, `deserializeSettings` читает её тем же словарём (`normalizeNumericSettings` + `applySettings`) и падает на `applyDefaults` при пустоте/битости → `schematicDifficulty`/`blockLead` больше не null при входе (NPE в `updateGeneratorSettings`/`PAPIHook` не стало). Диск и SQL теперь используют **один и тот же** набор ключей.
|
||
- **31. Схематики: креш на main thread от битых файлов.** `LPSchematicLegacy`/`SchemLoader` парсят без валидации; NPE/AIOOBE мимо `catch(IOException)` (SchematicManager.java:109-133, LpschemConverter.java:44-54). Битый файл = отказ включения плагина. → Валидация dimensions/palette/frameCount + `catch(RuntimeException)` + лимиты размера файла/блоков.
|
||
- **32. JumpCalculator: возврат невалидного прыжка.** После 10 неудач валидация сдаётся и возвращает заведомо невалидный блок (JumpCalculator.java:125-136). → Пере-роллить offset/направление, а не только уменьшать distance/height; при исчерпании — safety-fallback (простой допустимый шаг).
|
||
- **33. JumpCalculator: диагональный heading залипает.** `JumpDirector.getRecommendedHeading` в безопасной зоне возвращает current без изменений (JumpDirector.java:105-107), а `calculateJumpOffset` при `offset.getX()!=0` перезаписывает Z (JumpCalculator.java:110-115) → курс зигзагом и не выпрямляется. → При диагонали аппендикс не перезаписывать случайным; корректный поворот-домножитель.
|
||
|
||
### Генерация / lifecycle
|
||
|
||
- **34. Удаление неверных блоков схем.** `handleSchematicEndBlock` → `removeTrailBlocks(0)` удаляет старейшие 20 блоков курса, а не схематику (PlayerInteractionHandler.java:133-142); блоки схематики остаются в мире как скоробл. → Хранить координаты схематики отдельно и чистить их; не смешивать с history.
|
||
- **35. Ложные падения.** `fallThreshold = spawnY - 30` при спуске ~2 блока/прыжок → длинный спуск (>15 прыжков) ресетит живого игрока (LifecycleTickManager.java:72). → Считать порог относительно последнего блока / прогресса, а не спавна.
|
||
- **36. Генерация вхолостую.** `maintainBlockLead` сравнивает `score+2` с `history.size()`, но `cleanupDistantBlocks` срезает голову истории каждые 100 тиков → 5 блоков/тик генерится даже когда игрок стоит (LifecycleTickManager.java:89-98). → Считать lead по координате последнего сгенерированного блока, а не size истории.
|
||
- **37. One-time награды повторяются.** `collectedRewards` нигде не читается (LifecycleEventManager.java:82-86) → награда за N очков выдаётся в каждом забеге. → Проверять и помечать collectedRewards при выдаче.
|
||
- **38. Мёртвый конфиг генератора.** `jump-validation.*` (JumpValidator жёстко зашит: 4.5/4.1/1.25/3.0), `jump-types.*`, `ghost-mode.*`, `useSpecialBlocks`, `blockLead`, `EffectManager` — не читаются нигде. → Подключить или удалить из config.yml/generation.yml.
|
||
|
||
## 🟠 [P2] Высокое
|
||
|
||
### Потокобезопасность
|
||
|
||
- **39. `Divider.SECTIONS` — обычный HashMap, синхронизирован только add** (Divider.java:26,33-54); итерации на quit-патче и в листенерах без блокировки → CME. → ConcurrentHashMap или полная синхронизация read-путей.
|
||
- **40. `PlayerMetrics` не потокобезопасен** при том, что тик-поток пишет, а async-автосейв (5 мин) читает (MetricsCollector.java:148-190; jumpTypeStats — HashMap). → copy-on-read или атомарные поля/ConcurrentHashMap.
|
||
- **41. `FileStatsStorage` — гонка read-modify-write** между тиком (fall) и async-сейвом → потерянные апдейты/битые JSON (FileStatsStorage.java:53-67,94-114). → Сериализация записи (ReentrantLock / очередь сейвов).
|
||
- **42. `Leaderboard.put` сортирует вне лока**, а async-таймер мутирует карту (Leaderboard.java:83-96, LeaderboardStorage.java:63-73) → CME и окно пустого/неверного лидерборда. → Сортировка под `synchronized(scores)`; не хранить пре-сортированную карту.
|
||
- **43. `StorageDisk` — неатомарная запись файла** (таймер + синхронный shutdown пишут одновременно). → tmp-файл + rename, файловый лок.
|
||
- **44. `onChat` (async) итерирует сессионные ArrayList/`Divider.SECTIONS`** (ParkourRestrictionListener.java:36-60) → CME. → Снимок списков или синхронизация.
|
||
|
||
### Player
|
||
|
||
- **45. `PreviousData` не восстанавливает: скорость ходьбы/полёта, атрибуты, опыт, невидимость, питомцев, коллайдер, чужой скорборд** (PreviousData.java:26-95; ParkourPlayer.java:62-64 принудительно снимает flight/invisible). → Расширить capture/restore; восстановление исходного scoreboard.
|
||
- **46. `Storage.readPlayer` синхронно на main thread при join** (UserRegistry.java:46) — файл/DB блокируют тик. → Async-чтение с кэшем или схемой «join сразу, подгрузка настроек позже».
|
||
- **47. `UserRegistry.getUsers()` строит полный Set на каждый BlockBreak/Place/Drop/Damage** (UserRegistry.java:94-98) → O(n) аллокаций на горячем пути. → Кэш `Map<UUID, ParkourUser>` + привязка через PersistentDataContainer или entity.
|
||
- **48. `sendTranslated(...).formatted()` бросает IllegalFormatException при `%` в нике/сообщении** (ParkourUser.java:102, Locales.java:76-82). → Обернуть в try или использовать replace-based формат.
|
||
- **49. `getJoinCount` — int между main и async (bStats)** (UserRegistry.java:100-104). → AtomicInteger.
|
||
- **168. ✅ ИСПРАВЛЕНО (2026-10-06, см. ПРОГРЕСС-8): `ParkourUser.chatType` — состояние без записи.** Поле инициализировано
|
||
`PUBLIC`, читалось в `ParkourRestrictionListener.onChat` (switch `chatType()`), но сеттера не было нигде → ветки
|
||
`LOBBY_ONLY`/`PLAYERS_ONLY` были недостижимы. Оказалось, фича спроектирована, но не подключена: в `en.yml`/`ru.yml`
|
||
уже лежал готовый блок `settings.chat` (material/name/lore/values + formats), а `ParkourOption.CHAT("settings.chat", …)`
|
||
уже был в реестре опций. → Кнопка-цикл в `SettingsMenu` (слот 15) + `ParkourUser.setChatType(...)` + `ChatType.next()`.
|
||
|
||
### Modes
|
||
|
||
- **50. `GravityShiftMode` не снимает эффекты при выходе игрока** (только в reset для присутствующих, GravityShiftMode.java:149-163). → Снимать в unregister.
|
||
- **51. `RaceMode.finishRace` — `reset(false)` оставляет игрока в воздухе** (RaceMode.java:142-147; GeneratorCleanup.reset чистит курс без регенерации/телепорта). → Телепорт в лобби + регенерация старта.
|
||
- **52. `SpeedrunMode` оставляет удалённые блоки в `history`** → ломает координатные поиски. → Удалять координату из history при снятии блока.
|
||
- **167. ✅ ИСПРАВЛЕНО (2026-10-06, см. ПРОГРЕСС-8): Elytra-под-режимы не отличались от базового.**
|
||
`ElytraGenerator.getMode()` возвращал `Modes.elytra()`, а 5 под-генераторов (`Close/MinSpeed/TimeTrial/SpeedDemon/ObstacleGenerator`)
|
||
его не переопределяли → guard `getMode() instanceof CloseMode` в `create()` **никогда не был истинен**: повторная команда
|
||
в том же режиме не делала early-return, а пересоздавала сессию. Геттеры `Modes.speedDemon()/minSpeed()/timeTrial()/close()/obstacle()`
|
||
не вызывались нигде. → режим передаётся в конструктор `ElytraGenerator`, `getMode()` отдаёт его.
|
||
|
||
### Схематики (жизнь в игре)
|
||
|
||
- **53. Паста схематики синхронно на main thread** (BlockPlacer.java:192-226) — большая схема = фриз. → Async-паста с FastAsyncWorldEdit или chunk-снапшоты.
|
||
- **54. `StructurePaster` записывает в «схему» все не-air блоки бокса**, включая чужие (платформа, курс) (StructurePaster.java:44-54) → потом удаляются. → Записывать только блоки, поставленные самой пастой (сравнивать с snapshot до).
|
||
- **55. `SchemPaster` использует `getMinimumPoint()` вместо `getOrigin()`** (SchemPaster.java:27,36) → смещённая вставка. → `clipboard.getOrigin()`.
|
||
- **56. Загрузка всех схем синхронно на enable/reload** (SchematicManager.java:87-133). → Async-загрузка с прогрессом.
|
||
|
||
### Призы / награды
|
||
|
||
- **57. `%player%` через `String.replaceAll`** — `$`/`\` в нике кидают IllegalArgumentException (Reward.java:43-45). → `replace(..., literal)`.
|
||
- **58. `contains("vault:")` case-insensitive, но `replaceFirst("vault:")` — нет** → `VAULT:100` падает с NFE (Reward.java:55-61). → Единый lowercase-обработчик.
|
||
- **59. Нет валидации сумм Vault** — `vault:-100` уводит баланс в минус (Rewards.parseScores). → Клэмпить >=0 или блокировать отрицательные.
|
||
|
||
## 🟡 [P3] Среднее
|
||
|
||
### SQL / storage
|
||
|
||
- **60. `SQLDataMapper` UUID.fromString кидает unchecked IllegalArgumentException** (SQLDataMapper.java:65,31) мимо catch(SQLException) → битые строки роняют чтение. → Ловить и логировать/скипать строку.
|
||
- **61. `SQLQueryExecutor.prepareStatement`** (без callers) никогда не закрывает connection → утечка пула при будущем использовании. → try-with-resources / borrow-tracking.
|
||
- **62. `SQLMigrationManager` мёртв; `migration_adaptive.sql` ссылается на несуществующую `loparkour_scores` и нигде не исполняется**; та же DDL в 3 копиях; префикс `OptionSQL.prefix()` применён непоследовательно. → Единый мигратор с версионированием; починить/удалить SQL-файл.
|
||
- **63. `writeScores` гоняет всю таблицу туда-сюда** без дельт (StorageSQL.java:105-128). → Инкрементальные upsert'ы только изменённых записей.
|
||
- **64. `Score.fromString` — CSV по запятой: ник с `,` ломает все поля** (Score.java:19-31). → Экранирование или JSON.
|
||
- **65. `validateConnection` мутирует `OptionSQL.disable()` при транзиентном сбое** (SQLConnectionManager.java:87-92) — SQL отключается навсегда. → Отдельный флаг/ретрай.
|
||
|
||
### Adaptive
|
||
|
||
- **66. `NEAR_MISS_THRESHOLD=3.5` хардкод при живом конфиге `adaptive.near-miss-distance` (0.3)** (MetricsCollector.java:32-33); `getNearMissDistance/getUpdateInterval/getConfidenceThreshold` без callers. → Подключить конфиг.
|
||
- **67. Разные формулы confidence при записи и чтении** (SkillRating.java:54-62 vs SQLStatsStorage.java:48). → Единый метод.
|
||
- **68. Мёртвый API адаптива**: `estimateMinRating/estimateMaxRating/calculateRecommendedRating/isReadyForIncrease/shouldDecrease/smoothTransition/estimateDifficulty/getEffectiveRating`, `incrementJumps/incrementFalls/updateLongestStreak`, `PendingUpdate.flushBatch`, `NoOpStatsRepository`. → Удалить или до-реализовать.
|
||
- **69. `reload()` гоняет `saveAllMetrics()` на command thread** (AdaptiveServices.java:52-63). → Async.
|
||
|
||
### Config / reload
|
||
|
||
- **70. `/lp reload` не атомарен**: дублирует `Registry.STYLES` (никогда не чистится), оставляет старые `Rewards` при `enabled:false`, `OptionGeneral.borderSize()` устаревший (перечитывается только при `firstLoad`), миры/генераторы не обновляются (ConfigUpdater, OptionStyles.java:23-43, Rewards.java:41-52, OptionGeneral.java:44-50). → Полный reset registry + пересоздание зависимых подсистем.
|
||
- **71. `LocaleLoader` при ошибке подменяет кэш на emptyMap** → все сообщения пустые (LocaleCache.java:22-25). → Сохранять старый кэш при сбое.
|
||
- **72. Нет fallback на en** для пропущенных ключей/неизвестных локалей (Locales.java:69-71). → Fallback-цепочка locale → en.
|
||
- **73. `Rewards.init()` при `enabled:false` не чистит статические мапы** → старые награды продолжают выдаваться после reload. → Очистка.
|
||
- **74. `ConfigAccessor` типо-опечатка тихо даёт false/0** (ConfigAccessor.java:45-48). → Строгая валидация ключей при старте.
|
||
- **164. Секция `generation.*` в generation.yml игнорируется.** Живые веса берутся из `advanced.distance|height|type|special` (`GeneratorProfileManager`, `GenerationProfileLoader`), а `generation.type|normal|special` читал только мёртвый `OptionGeneration.initGenerationProbabilities()` — удалён на шаге инкапсуляции `config/options`. В shipped `generation.yml` обе секции дублируют друг друга (значения разные: 85/10/5 против 80/15/5) → пользователь правит то, что не работает. → Либо удалить `generation.type|normal|special` из generation.yml, либо сделать их источником для профилей; `generation.settings.min-y/max-y` остаются живыми. `ConfigLoader.validateConfigs` уже проверяет живые пути `advanced.*`.
|
||
- **165. Мёртвый конфиг частиц/звуков.** `particles.sound-type|sound-pitch|sound-volume|particle-type|particle-shape` читаются в `OptionParticles`, все 6 геттеров (`shape()`, `soundType()`, `particleData()`…) не вызываются нигде → настройки не влияют на игру (используются per-player `ParkourPlayer.particles/sound` из меню). → Подключить в рендере частиц/звуков (`util/particle/`, `HookSound`) или убрать секцию из config.yml.
|
||
- **166. Ещё два читаемых, но не потребляемых ключа.** `options.leads.amount` → `OptionGeneral.possibleLeads()` (валидация 1–128 и лог работают, список не читает никто — lead берётся из `default-values.leads.default`), `memory.block-cleanup-distance` → `OptionGeneration.blockCleanupDistance()` (дистанция чистки захардкожена в `GeneratorCleanup`). → Подключить или удалить.
|
||
|
||
### Session
|
||
|
||
- **75. `catch(Throwable)` вокруг `generator.tick()` без стека + сообщение игрокам** (SessionStateManager.java:80-83). → Логировать стек, не спамить чат.
|
||
- **76. `SessionUserManager` — целиком мёртвый дубликат SessionPlayerManager**; `updateVisibility` O(n²) showPlayer. → Удалить/объединить, кэшировать видимость.
|
||
- **77. `Session.generator` — публичное mutable поле** (Session.java:23). → private + getter.
|
||
|
||
### Menu / GUI
|
||
|
||
- **78. `currentPage` — общий поле-синглтон на всех игроков** (SingleMenu.java:20, SpectatorMenu, LeaderboardsMenu, SingleLeaderboardMenu) — двое листают → перезаписывают друг друга. → Per-player состояние (Map<UUID, Integer> или GUI-инстанс на игрока).
|
||
- **79. Скилл-головы на лидерборде не рендерятся**: `setOwningPlayer`, затем `skull.setItemMeta(built)` метой STONE (SingleLeaderboardMenu.java:53-80; `head.material` пуст). → Применять name/lore к той же SkullMeta, не перезаписывать.
|
||
- **80. `LangMenu` показывает первые 7 неупорядоченных ключей без пагинации** (LangMenu.java:37-54); `ParkourSettingsMenu` обрезает стили на 7. → Сортировка + пагинация.
|
||
- **81. Клик-обработчики не проверяют игрока** — общий стейт меню. → Привязка к игроку.
|
||
|
||
### Command / security
|
||
|
||
- **82. `getCommand("LoParkour")` без null-guard** (PluginBootstrap.java:118-119) — работает только из-за lowercase-fallback. → Null-проверка.
|
||
- **83. Схематик-хендлеры без собственных permission-проверок** (SchematicCommandHandler) — всё держится на вызывающем. → Проверка в каждом handler.
|
||
- **84. `SchematicCommandHandler.SELECTIONS` кэш ключён по Player и не чистится на выход** → утечка памяти (SchematicCommandHandler.java:38,58-71). → Удалять при quit/инвалидации.
|
||
- **85. Tab-completion предлагает отключённые режимы** (LoParkourCommand.java:232-236); `Registry.getMode` case-sensitive, лидерборд-аргумент — нет. → Унифицировать case/фильтр по enabled.
|
||
- **86. `admin`-нода даёт доступ к `recoverinventory` другого игрока и глобальному reset** (ParkourOption.java:49). → Разделить permission-ноды.
|
||
|
||
### Hooks / util
|
||
|
||
- **87. `HoloHook` читает `split[1],split[2]` без проверки длины** → AIOOBE (HoloHook.java:35-50). → Валидация аргументов.
|
||
- **88. PAPI expansion id = `"witp"`** (PAPIHook.java:27) — наследие форка «Walk in the Park» (оригинального Infinite Parkour), не совпадает с документацией/скрорбордом. → Переименовать в `loparkour` (см. п.118).
|
||
- **89. `VaultHook` кэширует Economy навсегда** (VaultHook.java:26-35) — после reload экономики ссылка битая. → Re-resolve при каждом использовании/слушать ServiceEvent.
|
||
- **90. `Probs.random()` на пустой карте — NoSuchElementException** (Probs.java:34-48; достижимо при инвертированных min>max или невалидных special-материалах); все нулевые веса → всегда первый элемент. → Empty-check + отсев нулевых весов.
|
||
- **91. `ParticleUtil.box` — бесконечный цикл при `spacing<=0`** (ParticleUtil.java:36-55). → Guard.
|
||
- **92. `Locations.max/min` и `SkullSetter` — мёртвый код**; `Item.clone()` теряет meta. → Удалить/починить.
|
||
- **93. `en.yml:258` — висячий плейсхолдер `server.ip` в скорборде**. → Убрать или подставить.
|
||
|
||
## ⚪ [P4] Низкое / мелочи
|
||
|
||
- **94. Миграция схем при загрузке нерекурсивна** и пишет `schematics.yml` при каждом конверте (LpschemConverter.java:63-84).
|
||
- **95. `SchematicIdGenerator` хеширует только byte-координаты → коллизии id** (SchematicIdGenerator.java:23-28).
|
||
- **96. `parseDifficulty` принимает NaN/Infinity** (SchematicCreator.java:64-70) → схема навсегда невыбираема.
|
||
- **97. `stem()` регистрирует файл `.nbt` под пустым id**; `.NBT` игнорируется, `.schematic` — case-insensitive (SchematicManager.java:147-150,88).
|
||
- **98. `Locales.getPlayerLocale`/`ParkourUser.locale` deref `OptionGeneral.optionsDefaults().get(LANG)` без null-check** (Locales.java:170-184, ParkourUser.java:64).
|
||
- **99. Магические числа**: fall-threshold -30 (LifecycleTickManager.java:72), render 24.0/0.1/%4 (InvisibleBarrierMode.java:67-68,98), 10/30 (GravityShiftMode.java:82,108), 100 тиков (RaceMode.java:147), hotbar delay 5 (ParkourPlayer.java:192), `TICKS_PER_SECOND=20` переопределён (SpeedrunMode.java:82). → В конфиг/константы.
|
||
- **100. Deprecated API**: `Material.STONE_SLAB` (MaterialUtil.java:20), `registerNewObjective(String,String,String)` (ScoreboardManager.java:75), `getOfflinePlayer(name)` (AdminCommandHandler.java:186-199), legacy ACTION_BAR (ParkourSpectator.java:73, RaceMode.java:103).
|
||
- **101. `ParkourSpectator.java:96` — бессмысленный double-set spectator target** (no-op).
|
||
- **102. `GeneratorStatistics.registerScore` пишет в лидерборд на каждого игрока при каждом падении** в мультиплеере (GeneratorStatistics.java:46-55).
|
||
- **103. `GeneratorProfileManager.calculateSpecialChances` — пустой `catch(Exception ignored)`** (GeneratorProfileManager.java:154) → бесшумно отключает special-блоки; magic-веса 0.3/0.2/0.5 (57-67).
|
||
- **104. `ProfileValue.asDouble/asInt` бросают NFE на нечисловых строках** (Profile.java:54,64) — только empty-guard.
|
||
- **105. Дубликат логики `create()` во всех режимах** с несогласованными проверками дублей (instanceof DefaultMode vs instanceof SpeedrunGenerator).
|
||
- **106. Мёртвый код**: `EffectManager` целиком, `SessionUserManager`, `SQLMigrationManager`, `SQLQueryExecutor`, `SkullSetter`, `Locations.max/min`, `ParkourBlockGenerateEvent`, `onPlayerJoin` (CoopMode.java:169), `PLAYER_COLUMNS`/`OptionContainer` (ParkourPlayer.java:38,205), `acceptingPlayers`-механизм, `Muted` в двух местах.
|
||
- **107. Публичные mutable поля-государство**: ✅ **ЧАСТИЧНО** — `GeneratorState` (15 полей) и весь кластер `player/*` (`ParkourUser`, `ParkourPlayer`, `PlayerSettings`) инкапсулированы в ПРОГРЕСС-6, `VisibilityModifier` переведён в `error`. Остались `ParkourGenerator.session/island/profile/state/generatorOptions` (публичные final — правило их разрешает, но состояние чужое мутируется) и `Score` времени в строке. → Инкапсуляция.
|
||
|
||
## 🧪 Тесты
|
||
|
||
- **108. Расширить покрытие** (сейчас 4 класса, 398 строк): минимум — `Score.getTimeMillis`, `JumpCalculator/JumpValidator` (включая retry-exhaustion и диагональ), `ConfigUpdater.merge` (листы/комментарии/пропуски), `Probs.random` (пустые/нулевые веса), `Reward` (vault-кейс, `%player%` с `$`), `SQLDataMapper` (UUID/настройки), `PreviousData` restore.
|
||
- **109. Проверять Folia-инварианты в CI** (запрет BukkitRunnable/global-мутаций) — статический анализ или правило checkstyle.
|
||
|
||
## 📊 Конкуренты — куда догонять
|
||
|
||
- **110. Режимы**: IP-Reborn имеет 8 рабочих режимов (Practice/Speed/SuperJump/Hourglass/TimeTrial/Wave/TeamSurvival/Duels) + elytra. У нас 5 работают, coop мёртв, elytra удалён. → Довести coop до рабочего состояния или убрать из рекламы; рассмотреть TimeTrial/Hourglass.
|
||
- **111. Мультиплеер-лобби с инвайтами** — у IP-Reborn есть, у нас нет (только dead coop). → Реализовать invite-флоу.
|
||
- **112. Void-генератор**: IP-Reborn встроил native void-gen (не нужен VoidGen). У нас зависимость от VoidGen в softdepend и внешнего мира. → Встроить void-gen.
|
||
- **113. 6 типов прыжков (ice/slab/fence/glass/schematic)** — у нас slab/ice/schematic есть, fence/glass-pane через jump-types (мёртвый конфиг). → Догнать типологию прыжков.
|
||
- **114. Документация/вики + Discord** — у конкурентов полноценная вики. → Сделать документацию по config.yml, generation.yml, rewards-v2.yml, локалям, API.
|
||
- **115. Версионность**: конкурент держит 1.7–26.x. Наш api-version 1.20, Java 21+. → Определить целевые версии и CI-тесты на них.
|
||
- **116. Инвентарь-recovery как фича поддержки** — вынести в команду и проверять (у нас есть, но с гонками потоков, см. InventoryData.java:50-57).
|
||
|
||
### Наследие форка (LoParkour = форк «Walk in the Park» / Infinite Parkour Efnilite)
|
||
|
||
- ~~**117. Удалить формат `.lpschem` полностью...**~~ ✅ **ВЫПОЛНЕНО** — пакет `schematic/legacy/lpschem/` (LPSchematicLegacy + pojo/), `LpschemConverter`, конвертер-команда (`/lp schematic convert`), `fromLegacy`, ветки convert в dispatcher/tab/messenger/локалях и упоминания в док. Остались только `.schem` (WorldEdit) + `.nbt`.
|
||
- **118. Переименовать PAPI-id `witp` → `loparkour`** (наследие оригинала; также поменять примеры в scoreboard/docs).
|
||
- **119. Command block support** — у IP и IP-Reborn есть (запуск команд `/parkour` из командного блока), у нас нет. → Поддержать `CommandBlock` sender в `LoParkourCommand`.
|
||
- **120. Расширить PAPI-плейсхолдеры до 15+** (у IP 15+): score, record, время, режим, ранк, стиль, скорость, статистика адаптива и т.д.
|
||
- **121. «No-lag scoreboard»** — у IP заявлен, у нас скорборд пересобирается каждый тик (LifecycleTickManager.java:61, ScoreboardManager.update). → Обновлять title/lines только при изменении значений (debounce/версия).
|
||
|
||
### Таблица сравнения (полная)
|
||
|
||
| Возможность | LoParkour 1.3.3 | Ориг. Infinite Parkour 5.3.1 (заброшен, 10.2024) | IP-Reborn 6.0 | ajParkour 2.12 (free) | Parkour (A5H73Y) 7.2.8 |
|
||
|---|---|---|---|---|---|
|
||
| Бесконечная генерация | ✅ | ✅ (6 типов) | ✅ (6 типов) | ✅ в зоне (WorldEdit) | ❌ (карты/чекпоинты) |
|
||
| Режимы | 7 заявлено, coop мёртв | ✅ | 8 + elytra | 5 уровней difficulty | ❌ |
|
||
| Ghost-реплей | ❌ мёртвый код | ❌ | ❌ | ❌ | ❌ |
|
||
| Схематики | ✅ (.lpschem/.schem, крешит) | ✅ | ✅ | ❌ | ✅ паркур-блоки |
|
||
| Лидерборды | ✅ (TIME сломан) | ✅ | ✅ | ✅ top-10 | ✅ время/смерти/уровень |
|
||
| Storage | YAML+MySQL (утечки) | Local+MySQL | Local+MySQL | YAML+MySQL | YAML+MySQL |
|
||
| Inventory recovery | ✅ частично | ✅ | ✅ | ✅ | ❌ |
|
||
| Мультиплеер-лобби | ❌ coop мёртв | ❌ (IPPlus отдельно) | ✅ сессии/инвайты | ❌ | ❌ |
|
||
| PAPI / Vault / HD / Floodgate | ✅ (id witp) | ✅ 15+ PAPI | ✅ | ✅ | ✅ широчайшие |
|
||
| Command block | ❌ | ✅ | ✅ | ❌ | ❌ |
|
||
| Переводы | en/ru, без fallback | en/nl + custom | en/zh + custom | messages.yml | strings.yml |
|
||
| Dev API | ✅ события + Registry | ✅ | ✅ | ❌ | ✅ лучшая документированность |
|
||
| Folia | ❌ заявлено, не работает | ❌ | ❌ честно отключено | ❌ | ❌ |
|
||
| Версии | 1.19.2+ / Java 21 | 1.16–1.21 | 1.21.11–26.x / Java 21-25 | 1.7.10–26.x | 1.7–26.x |
|
||
| Актуальность | 2026, 1.3.3 | заброшен | v6, активный | 178 обновлений | с 2012 |
|
||
| Документация | минимальная | вики (осталась) | вики + дискорд | вики | полная вики |
|
||
|
||
> Ссылки: IP-Reborn — https://www.spigotmc.org/resources/infinite-parkour-reborn-3-in-1.136046/ + https://github.com/LostUmbrella58/IP-Reborn ·
|
||
> Ориг. IP — https://www.spigotmc.org/resources/87226/ + https://github.com/Efnilite/Walk-in-the-Park
|
||
|
||
---
|
||
|
||
## 🆕 Новые режимы (анализ исходников IP-Reborn в `ref/IP-Reborn-main/`)
|
||
|
||
### Duels (перенос из `dev.efnilite.ipp`)
|
||
|
||
- **122. Duels — модель «генератор на игрока».** ✅ **СДЕЛАНО**: `duels/` — `DuelsMode implements MultiMode`, `DuelsGenerator extends ParkourGenerator` (владелец: `Map<ParkourPlayer, SingleDuelsGenerator>`, allowJoining, goal, countdown), `SingleDuelsGenerator extends ParkourGenerator` (`getPlayers()=List.of(свой игрок)` → независимый state/history/score; при `state.score >= goal` → `owningGenerator.win`), `DuelsCountdown` (10с отсчёт + старт трасс). Результаты НЕ пишутся в лидерборд (`getLeaderboard()=null`). Тик через владельца (`tick()` тикает каждый single). Зарегистрирован (Modes.DUELS, PluginBootstrap, config.yml `modes.duels.{enabled,max-players,goal,island-distance}`, локали `play.multi.duels.*`).
|
||
- **123. Duels — изменения ядра.** ✅ **Добор**: добавлен хук `ParkourGenerator.selectBlockData()` (public, возвращает null по умолчанию) → `BlockSelector.selectBlockData()` первым делом проверяет его; `SingleDuelsGenerator` переопределяет → возвращает цвет игрока (16 concrete-цветов, `setPlayerIndex`) и форсит `defaultChances={DEFAULT:1.0}` + пустые specialChances (курс одноцветный, без спец/схем). Арены — плоские stone-платформы на сдвинутом Z (Island не трогали).
|
||
- **124. Duels — классы:** `DuelsMode implements MultiMode` (create() реально создаёт сессию, join() через `isAcceptingPlayers`, **leave() только `removePlayer` без `ParkourUser.leave`** — фикс рекурсии Coop), `DuelsGenerator` (owner/allowJoining/playerGenerators/spawnData/goal; `initCountdown()` по клику LIME_BANNER 10с; `win()` с guard `stopped`; `reset(false)` синхронно и идемпотентно, НЕ `runLater`), `SingleDuelsGenerator` (`getPlayers()=List.of(player)`, `setPlayerIndex`→цвет, `score()`→`win` при goal).
|
||
- **125. Duels — интеграция.** Частично: `modes.duels{enabled,max-players,goal,island-distance}` в config.yml, локали `play.multi.duels.*`, баннер-старт владельцу. ✅ **Добор**: `menu/lobby/DuelsMenu` (список открытых дуэльных сессий из Divider.SECTIONS, клик→join) + команда `/parkour duels` (CommandRouter/CommandTabCompleter) + локали `play.multi.duels.{name,menu_empty}`. Осталось: кнопка в PlayMenu.
|
||
- **126. Duels — не повторить баги:** Race (TIME-лидерборд, ресет в воздухе через `runLater`) — у Duels лидерборда нет, время брать из `state.start` своим форматтером, ресет только синхронно с null-guard; Coop (рекурсия, невозможность создать) — не вызывать `ParkourUser.leave` поверх unregister, сессию создавать в `create()`.
|
||
|
||
### Elytra (порт из `dev.efnilite.iep`, Kotlin → Java)
|
||
|
||
- **127. Elytra — модель.** ~~...~~ ✅ **БАЗОВОЕ СДЕЛАНО**: `elytra/section/` (ElytraSection+KnotDirector+PointType+ClientBlockChanger, сплайн через commons-math3), `elytra/generator/ElytraGenerator extends ParkourGenerator` (свой tick/score/фолл/reset, sustained-буст +0.08 в ascending), `elytra/mode/ElytraMode`. Зарегистрирован (Modes.ELYTRA, PluginBootstrap, локали play.elytra.elytra). Команда commons-math3: `org.apache.commons:commons-math3:3.6.1` + relocate. Осталось по п.127: фейерверк-буст (`v*0.7+dir*0.6`) — сделано упрощённо (только sustained); CloseMode (FLAT-кольца).
|
||
- **128. Elytra — клиент-сайд рендер.** ✅ `ClientBlockChanger`: очередь `Map<chunkX, Set<Vector>>`, `player.sendBlockChanges(states)` по чанкам, forward limit min(vd,8), flush/clear. Секции хранят Vector (без пиннинга чанков).
|
||
- **129. Elytra — классы.** ✅ Базовый: ElytraSection/KnotDirector/PointType/ClientBlockChanger + ElytraGenerator + ElytraMode. ✅ **M-добор**: подклассы-режимы SpeedDemonGenerator (скор = макс. скорость), MinSpeedGenerator (поддержание мин. скорости, ресет при 6 тиках медленнее), TimeTrialGenerator (скор ограничен SCORE из конфига), CloseGenerator (FLAT-кольца); режимы SpeedDemonMode/MinSpeedMode/TimeTrialMode/CloseMode + регистрация + локали `play.elytra.{speeddemon,minspeed,timetrial,close}` + config `mode-settings.{min-speed,time-trial}`. Добавлен `Config.getDouble(path, default)`. ✅ **Ещё доборы**: **фейерверк-буст** `elytra/generator/ElytraBoost` (разовый толчок v*0.7+dir*0.6 + частицы FIREWORKS_SPARK + звуки ракеты при входе в ascending, sustained +0.08/тик, трейл каждые 4 тика; Particle.FIREWORK нет в API 1.20.4 → FIREWORKS_SPARK); **Obstacle-режим** (ObstacleGenerator с enum Obstacle PILLARS/LINES/HOLE_IN_WALL/CIRCLE/DIAMOND, клиент-сайд через ClientBlockChanger.sendNow, + ObstacleMode + локали `play.elytra.obstacle`). Осталось: раздача элитр через PreviousData.
|
||
- **130. Elytra — зависимости/мир.** ✅ commons-math3 подключён+relocated; отдельный мир не нужен (клиент-сайд); ASCEND_THRESHOLD_Y=50; скор в double (movementScore), в лидерборд пока не пишется (TODO: int-масштаб). Сложность: **S ✅**, M (режимы+бусты) и L (Obstacle+настройки+стили+награды) — далее.
|
||
|
||
- **131. Тесты под новые режимы.** ✅ **СДЕЛАНО** (JUnit5+Mockito, 15 новых тестов): `ElytraSectionTest` (сплайн монотонен по X, 5 узлов, isNearPoint, generateBlocks→chunkX, clone), `ElytraPointTypeTest` (CIRCLE 0→1 точка / radius3→24 симметрично, FLAT→3, heightOffset), `ElytraKnotDirectorTest` (bounds dx/dy/dz при seed, разные seed≠), `SingleDuelsGeneratorTest` (16 цветов, fallback, getPlayers без owner). Итог: **41 тест, 0 fail**. Осталось: Coop-регрессия unregister (как п.131 упоминал) — можно добавить позже.
|
||
|
||
---
|
||
|
||
## 🧹 Зачистка варнингов линтеров (добавлено после ужесточения конфигов)
|
||
|
||
Checkstyle отработал `0 errors` (error-уровневые legacy-нарушения `IllegalCatch` / `NPathComplexity` / `WhitespaceAround` закрыты супрессиями в `config/checkstyle/suppressions.xml`), но остаётся **1429 warning**. PMD — **45 нарушений** (report-only, `isIgnoreFailures=true`). Статистика по файлам — в `build/reports/checkstyle/main.xml` и `build/reports/pmd/main.xml`.
|
||
|
||
> ## ✅ ПРОГРЕСС (этап «декомпозиция god-классов» ЗАВЕРШЁН)
|
||
> Документ структуры: `docs/ARCHITECTURE.md`. Все 11 god-классов >200 строк декомпозированы
|
||
> (супрессии FileLength сняты, **каждый файл ≤200 строк**): LoParkourCommand→CommandRouter/CommandTabCompleter/
|
||
> JoinCommandExecutor/LeaderboardCommandExecutor/CommandUtil; SchematicCommandHandler→SchematicDispatcher/SchematicHandler/
|
||
> SchematicWand/SelectionStore/SchematicMessenger; ConfigUpdater→ConfigMergeParser/ConfigMergeProcessor;
|
||
> BlockPlacer→BlockTargetSelector/BlockWorldPlacer/SchematicBlockPlacer; InvisibleBarrierMode→BarrierGenerator/BarrierRenderer;
|
||
> PAPIHook→PlaceholderDispatcher&Resolvers&Formatters; AdminCommandHandler→ForceJoin/ForceLeave/Reset/RecoverInventory/
|
||
> AdminTargetResolver/AdminResponses; MetricsCollector→MetricsCacheStore/MetricEventCollector/JumpTimingTracker;
|
||
> StorageSQL→SQLSchemaManager/SQLScoreRepository/SQLPlayerRepository; ParkourPlayer→ScoringTracker/Persistence/SpawnSetup;
|
||
> Leaderboard→LeaderboardRecords.
|
||
> Актуальные счётчики: **checkstyle 0 errors / 1413 warnings**, **PMD 34**. Пункты 132–138 — в силе.
|
||
>
|
||
> ## ✅ ПРОГРЕСС-3 (Этап 3 «зачистка варнингов» — bulk завершён)
|
||
> 6 сабагентов по кластерам пакетов. Итог: **checkstyle warnings 1373 → 21**,
|
||
> **PMD 33 → 9** (build ✓, тесты проходят).
|
||
> - **FinalLocalVariable — 991 → 0** (только у не-переприсваиваемых локалок; переприсваиваемые не тронуты).
|
||
> - **MagicNumber — 340 → 0** (константы `private static final`, тип = тип литерала).
|
||
> - **EmptyLineSeparator / MissingDeprecated — 0**; исправлены 15 методических PMD
|
||
> (CognitiveComplexity, CyclomaticComplexity, AvoidReassigningParameters, EmptyCatchBlock).
|
||
> - Осталось ~30 дизайн-метрик на god-классах: checkstyle ClassFanOutComplexity(18)+
|
||
> ClassDataAbstractionCoupling(2, PluginBootstrap/Menus) и PMD TooManyMethods(8)+GodClass(1,
|
||
> CommandTabCompleter). Это сигналы к декомпозиции, не тривиальные фиксы — решаем отдельно
|
||
> (пороги правил vs декомпозиция).
|
||
> - Итог по TODO: п.132 (FinalLocalVariable) и п.133 (MagicNumber) фактически ✅; п.134–137 — почти
|
||
> ✅ (остаток только дизайн-метрики). п.138 (PMD→гейт) — только после решения по п.132–137.
|
||
>
|
||
> ## ✅ ПРОГРЕСС-4 (пороги подняты под реальность — ВСЁ чисто)
|
||
> - `ClassFanOutComplexity` max 10→**20**, `ClassDataAbstractionCoupling` max 7→**20** (checkstyle,
|
||
> остаются warning-подсказками); `TooManyMethods` maxMethods 10→**15** (PMD); `GodClass` убран из
|
||
> ruleset (составная метрика без порога, дублирует TooManyMethods+CognitiveComplexity).
|
||
> - **checkstyle: 0 errors / 0 warnings. PMD: 0 violations. build ✓ (тесты проходят).**
|
||
> - Жёсткие гейты не ослаблены: FileLength 200, NPath 50, MethodLength 80, LineLength 150,
|
||
> CyclomaticComplexity 20, ParameterNumber 7 — без изменений.
|
||
> - Остаётся п.138: перевести PMD из report-only в жёсткий гейт (`isIgnoreFailures=false`).
|
||
>
|
||
> ## ✅ ПРОГРЕСС-2 (правило «одна папка = одна цель» — применено полностью)
|
||
> Правило в `docs/ARCHITECTURE.md` (п.8). Выполнено разбиение переполненных пакетов: `command/`→core/+util/,
|
||
> `mode/impl/`→solo/+multi/+barrier/, `hook/papi/`→resolver/, `storage/sql/`→schema/+query/+repo/,
|
||
> `adaptive/core/`→calc/+metrics/+model/, `api/event/`→session/+score/+generation/, `config/core/`→merge/,
|
||
> `config/options/`→section/, `player/service/`→ui/, `menu/core/`→screen/, `util/gui/`→`menu/gui/`,
|
||
> `schematic/legacy/lpschem/`→pojo/. Пересекающие пакеты классы стали public (сигнатуры не менялись).
|
||
> Сборка: **checkstyle 0 errors / 1413 warnings, PMD 34, build ✓**.
|
||
|
||
- **132. [C] Почистить `FinalLocalVariable` (legacy, ~1034 warning / 120 файлов).** Объявить локальные переменные `final`. Делать вместе с декомпозицией, а не отдельным проходом.
|
||
- **133. [C] Почистить `MagicNumber` (legacy, ~346 warning / 60 файлов).** Заменить магические числа на именованные константы (приоритет: конфиг- и стиль-коэффициенты, пороги сложности, тайминги).
|
||
- **134. [M] Починить `WhitespaceAround` (legacy, 30 / 14 файлов).** Авто-формат либо ручная правка по отчёту.
|
||
- **135. [M] Починить `NPathComplexity` (legacy, 29 / 25 файлов).** Решается декомпозицией методов (файлы и так в списке на рефакторинг).
|
||
- **136. [M] Починить `IllegalCatch` (legacy, 22 / 19 файлов).** catch(Exception/Throwable/RuntimeException)→сужение до конкретных исключений либо переименовать в `expected|ignore`.
|
||
- **137. [M] Починить все нарушения PMD (45):** `CognitiveComplexity(17)` + `CyclomaticComplexity(7)` — декомпозиция; `GodClass(4)` + `TooManyMethods(8)` — декомпозиция; `AvoidReassigningParameters(7)` — переименование параметров; `EmptyCatchBlock(2)` — комментарий/конкретный тип.
|
||
- **138. [S] Перевести PMD из report-only в жёсткий гейт.** После зачистки убрать `isIgnoreFailures=true` в `build.gradle.kts` (pmd-блок). ✅ сделано в `b771cc1`.
|
||
- **169. [M] Починить `ReturnCount` (69 warning / 52 файла) — ПЕРЕСТРУКТУРИРОВАНИЕМ, не порогом.** Все 69 — это половина
|
||
правила про void (`maxForVoid` по умолчанию = 1 ⇒ запрещена любая guard-клауза `if (x) { return; }`); для value-методов
|
||
при `max=5` нарушений 0. Владелец запретил поднимать порог → в каждом из 69 void-методов убираем ранний `return`
|
||
(условие становится обычным `if`-блоком) либо выносим тело в helper-метод. Следить за `NestedIfDepth` (error, max=2)
|
||
и `NPathComplexity`. После обнуления — перевести `ReturnCount` в `error`.
|
||
- **170. [M] Починить `IllegalCatch` (23 warning / 18 файлов).** 19× `catch (Exception)`, 3× `catch (Throwable)`,
|
||
1× `catch (RuntimeException)`. Там, где конкретика реальна, — сужать до `IOException`/`NumberFormatException`/
|
||
`SQLException`/`YAMLException` и т. п. На истинных границах (диспетчер команд `LoParkourCommand:36`, `session-state:84`,
|
||
`LoParkour:128`) `Throwable` **обязан** стать `RuntimeException`: `Throwable` глотает `Error` (OOM, StackOverflow) —
|
||
это баг, а не стиль. Пограничные `catch (Exception)` в `onEnable/onDisable`, загрузчиках конфигов/схематчей и
|
||
`SchematicManager` — оставляем широкими осознанно (релоад плагина не должен ронять сервер), но каждый такой site
|
||
требует комментария «почему именно Exception»; после разбора правило остаётся в warning на незачищенном остатке.
|
||
|
||
После зачистки п.132–137 — удалить соответствующие записи из `config/checkstyle/suppressions.xml` (супрессии остаются только для ещё незачищенных legacy-файлов).
|
||
|
||
---
|
||
|
||
## 🎯 Порядок работ: Задача 1 → Задача 2 → остальное
|
||
|
||
> Задано владельцем 2026-10-06. Всё остальное в этом файле (версии 1.19.2–26.3, смена команды, пункты аудита) делается **после** задач 1 и 2,
|
||
> потому что и lolib-слой, и структура пакетов затрагивают почти все файлы.
|
||
|
||
### Задача 1. Отказаться от `lolib` (`libs/lolib-3.0.0.jar`)
|
||
|
||
Почему: исходников и репозитория lolib нет (архив/утерян), jar лежит в `libs/` как бинарник, байткод 21 (мешает п.139 про Java 17), внутри — fat-jar с чужими зависимостями.
|
||
Мы тащим его целиком (`implementation(files("libs/lolib-3.0.0.jar"))` + релокация `dev.lolib`), а используем **5 классов в 28 файлах**:
|
||
|
||
| Что берём из lolib | Где (файлов) | Замена своим кодом |
|
||
|---|---|---|
|
||
| `dev.lolib.gui.InventoryGUI` | ~12 (все `menu/*`: `Menu`, `LPMenu`, `MainMenu`, `LobbyMenu`, `SettingsMenu`, `LangMenu`, `SpectatorMenu`, `DuelsMenu`, лидерборд-меню, `PlayerManagementMenu`, `ParkourSettingsMenu`) | собственный `menu/gui/` (инвентарь + `InventoryClickEvent`-листенер, слоты/страницы; per-player состояние — заодно закрывает п.78, п.81) |
|
||
| `dev.lolib.scheduler.Scheduler` / `ScheduledTask` | ~16 (`SessionStateManager`, `GhostPlayer/GhostReplay`, `RaceMode`, `SpeedrunMode`, `ParkourSpectator`, `PlayerSpawnSetup`, `InventoryData`, `Schematics`, `StorageSQL`, `LeaderboardStorage`, `World`…) | тонкая обёртка `scheduler/` над Bukkit + Folia-схедулерами (global/region/entity/async) — фундамент для п.21/143; без неё Folia не починить |
|
||
| `dev.lolib.worlds.VoidGenerator` | `World.java` | свой `ChunkGenerator` (void) в `world/` — это п.112/145 |
|
||
| `dev.lolib.core.LoPlugin` | `LoParkour.java` (+`PluginBootstrap`) | перейти на обычный `JavaPlugin` (что именно давал `LoPlugin` — проверить декомпиляцией: порядок enable/disable, автосервисы) |
|
||
|
||
Шаги:
|
||
1. **Выяснить фактическую поверхность API:** `javap`/декомпилятор по 5 классам, список методов, реально вызываемых из нашего кода (`grep` по `dev.lolib` — всё остальное внутри jar нам не нужно).
|
||
2. **Транзитивные библиотеки:** lolib — fat-jar (gson, HikariCP, caffeine, snakeyaml, slf4j, checkerframework). Сейчас gson/Hikari у нас и так объявлены явно (`FileStatsStorage`, `StorageDisk`, `SQLDataMapper`, `SQLConnectionManager`, `ParkourPlayer`); **caffeine/snakeyaml/slf4j напрямую не импортируются** — убедиться, что без jar сборка и тесты проходят, не добавлять лишнего.
|
||
3. Написать замены (scheduler → void-gen → gui → `JavaPlugin`), по одной, с тестами; после каждой — `./gradlew build`.
|
||
4. Удалить `libs/lolib-3.0.0.jar`, `implementation(files(...))`, релокацию `dev.lolib` и закомментированный `exclude` в `shadowJar`; убрать `softdepend: VoidGen` из `plugin.yml`.
|
||
5. Проверка: jar без `dev/loki/loparkour/lib/lolib/**` (`unzip -l`), плагин стартует на 1.21.x/26.x (см. п.141), размер jar заметно меньше.
|
||
6. Папка `libs/` остаётся пустой → удалить, если больше ничего не лежит.
|
||
|
||
Критерий готовности: `grep -r "dev.lolib" src build.gradle.kts` пусто, `build` зелёный, меню/таймеры/мир работают.
|
||
|
||
> ## ✅ ПРОГРЕСС — Задача 1 ВЫПОЛНЕНА (2026-10-06)
|
||
> Поверхность API уточнена декомпиляцией (`javap`): реально использовались `InventoryGUI` (create/title/rows/fill/setItem/open + GUIManager-листенер),
|
||
> `Scheduler.get().run/runAsync/runLater/runTimer` + `ScheduledTask.cancel()`, `VoidGenerator.getGenerator()`, `LoPlugin.enable/disable` + `loLogger()`
|
||
> (`AutoRegister`/`dependencies`/`load` — no-op, в коде аннотаций нет). Замены (по шагам, `build` зелёный после каждого):
|
||
> - **`scheduler/`** — `Scheduler` (фасад, кэш по плагину, `Backend`), `ScheduledTask` (functional interface, только `cancel()` — `isCancelled()` нигде не вызывался),
|
||
> `BukkitBackend` (Bukkit-планировщик), `FoliaBackend` (global/async-рефлексия: `RegionizedServer`-детект, fallback с warning — Folia API нет в Spigot/Paper артефактах матрицы).
|
||
> Entity/region-варианты (`runAt*`) не реализовывались — их никто не вызывает; добавить при п.143.
|
||
> - **`world/core/VoidGenerator`** — `ChunkGenerator` без генерации (`shouldGenerate*` = false; `shouldGenerateBedrock()` не переопределяется — deprecated в 1.19.2/1.20.4, default и так false).
|
||
> - **`menu/gui/inventory/`** — `InventoryGui` (тот же флюентный API, что и у lolib: call-site'ы меняли только import) + `GuiManager`
|
||
> (UUID→GUI, dispatch по raw slot, отмена кликов, чистка при close/quit). Удалён мёртвый `menu/gui/Menu.java` (legacy-стаб с javadoc на LoLib).
|
||
> - **`LoParkour extends JavaPlugin`** — `enable/disable` → `onEnable/onDisable`, `loLogger()` → `getLogger()` (+`log(Level, …)` в `PluginBootstrap`).
|
||
> - **Удалено:** `libs/lolib-3.0.0.jar` (+папка `libs/`), `implementation(files(...))`, релокация `dev.lolib`, закомментированный `minimize/exclude`,
|
||
> `softdepend: VoidGen`, строка `!libs/*.jar` в `.gitignore`, запись `api_docs/`, упоминания LoLib в комментариях/javadoc/CI.
|
||
> - **Проверки:** `grep -r "dev.lolib" src build.gradle.kts` пусто; `unzip -l` — нет ни `dev/lolib`, ни `dev/loki/loparkour/lib/lolib`;
|
||
> **jar 9 770 813 → 7 816 441 байт (−20%)**; `build` зелёный: checkstyle **0 errors / 0 warnings**, PMD **0**, **87 тестов / 0 fail**;
|
||
> матрица собирается: 1.19.2, 1.20.1, 1.21, 1.21.11 (JDK 21) и 26.1.2, 26.2 (JDK 25).
|
||
> - **Новые тесты:** `scheduler/SchedulerTest` (нет Folia-детекта на обычном JVM, кэш по плагину), `menu/gui/inventory/InventoryGuiTest`
|
||
> (границы rows, fill не затрагивает поставленное, dispatch+cancel клика), `world/core/VoidGeneratorTest` (синглтон, ничего не генерирует).
|
||
> - **Smoke-тест ПРОЙДЕН** (`./gradlew runServer`, Paper 1.21.4): `Loading` → `Enabling LoParkour` → конфиги синхронизированы,
|
||
> PAPI-хук зарегистрирован, паркур-мир создан → `Done (27.831s)`, `Disabling` без единого ERROR/Exception/WARN в логе.
|
||
> - **Остаток:** Folia-путь `FoliaBackend` не проверялся на Folia-сервере (п.143).
|
||
|
||
### Задача 2. Жёсткий рефакторинг под KISS / DRY / SOLID (сразу после задачи 1)
|
||
|
||
Правила (становятся **жёсткими гейтами**, не рекомендациями; расширяют `docs/ARCHITECTURE.md` п.5 и п.8):
|
||
- **≤ 4 файла в папке** (было «ориентир 3–5»). Больше → делим на подпапки по смыслу. Сейчас: 89 пакетов, **6 превышают 4**: `elytra/generator` (7), `elytra/mode` (6), `command/schematic` (6), `player/core` (5), `generator/jump/placement` (5), `generator/jump/calculation` (5).
|
||
- **≤ 200 строк на файл** — гейт уже есть (`FileLength` 200, error); проверить, что супрессий на FileLength не осталось; снизить `MethodLength` 80 → ~40.
|
||
- **KISS:** убрать абстракции с одной реализацией, пустые интерфейсы, неиспользуемые слои; метод делает одно (cyclomatic ≤ 10 вместо 20, `NPath` ≤ 50 → ≤ 30, вложенность if ≤ 2).
|
||
- **DRY:** убрать дубли (`create()` во всех режимах — п.105, дубликаты в SQL DDL — п.62, `SessionUserManager` ≈ `SessionPlayerManager` — п.76, дубли `canJump` и т.п.). Гейт — PMD CPD (минимум ~60 токенов).
|
||
- **SOLID:** SRP (god-классы уже порезаны, закрепить `TooManyMethods` ≤ 10, `TooManyFields`, `GodClass` вернуть), DIP (домен не импортирует `org.bukkit` там, где можно через порт), ISP (узкие интерфейсы), OCP (новые режимы без правок ядра).
|
||
- **Правило слоёв** (`api → core → subsystems → platform`) проверять автоматически (см. ниже), а не только в документе.
|
||
|
||
Настройка линтеров (усилить «сильно»):
|
||
- **Checkstyle:** `FinalLocalVariable`, `MagicNumber`, `ClassFanOutComplexity` (сейчас 20 → 12), `ClassDataAbstractionCoupling` (20 → 10), `ParameterNumber` 7 → 5, `ReturnCount`, `NestedIfDepth` 2 → 1–2, `IllegalCatch`, `IllegalThrows`, `HiddenField`, `VisibilityModifier` (закрывает п.77/107 — публичные mutable-поля), `DesignForExtension`/`FinalClass`, `JavadocMethod` для публичного API; **все warning → error**, `suppressions.xml` обнулить (сейчас 87 строк).
|
||
- **PMD 7:** включить категории `bestpractices`, `errorprone`, `design`, `multithreading`, `performance`, `security` целиком (а не выборочные правила); вернуть `GodClass`, добавить `CouplingBetweenObjects`, `LawOfDemeter` (по месту), `AvoidSynchronizedAtMethodLevel`, `DoNotUseThreads`, `AvoidUsingVolatile`; запустить и на `src/test`; `isIgnoreFailures=false` уже стоит — оставить.
|
||
- **Новые проверки (предложение, выбрать):**
|
||
1. **ArchUnit** (тест в JUnit): слои, отсутствие циклов между пакетами, «в папке ≤ 4 файла» и «≤ 200 строк» как тест, запрет `BukkitRunnable`/`Bukkit.getScheduler()` вне `scheduler/` (п.109 — Folia-инварианты), запрет `org.bukkit` в `api`/`core`.
|
||
2. **SpotBugs** (+ `find-sec-bugs`) — NPE/гонки/SQL-инъекции, закрывает целый класс пунктов P2 по потокобезопасности.
|
||
3. **Error Prone + NullAway** (compile-time) — null-safety на всём коде, `@Nullable` на границах.
|
||
4. **PMD CPD** — гейт на дубли (DRY).
|
||
5. **JaCoCo** — минимальное покрытие (старт 50% строк, ядро — 70%), отчёт в CI; п.108 (тесты) становится обязательным.
|
||
6. **Spotless** (google-java-format/palantir) — единый формат, `spotlessCheck` в CI; убирает `WhitespaceAround`/`EmptyLineSeparator` шум.
|
||
7. **OWASP dependency-check** или `gradle-versions`/`dependabot` — уязвимости/устаревшие зависимости (HikariCP 5.1.0, mysql-connector 8.3.0, bstats).
|
||
8. **forbidden-apis** — запрет deprecated/запрещённых вызовов (`String.format` без Locale, `Thread.sleep`, `System.out`).
|
||
9. **Gradle `dependency-analysis`** — лишние/недостающие зависимости (поймает остатки lolib).
|
||
10. **Проверка дат/версий:** тест, что `plugin.yml`/`build.gradle.kts`/README/CHANGELOG версии совпадают.
|
||
- **CI:** `quality`-job запускает всё выше (`checkstyleMain checkstyleTest pmdMain pmdTest spotbugsMain test jacocoTestCoverageVerification archTest`) и валит PR; на `main` — ещё и матрица версий.
|
||
|
||
Порядок: (a) настроить линтеры и **получить красный отчёт** (честный baseline) → (b) зафиксировать список нарушений по пакетам → (c) рефакторить пакет за пакетом (папка ≤ 4, файл ≤ 200) → (d) вычищать супрессии → (e) включить гейты в CI. Нельзя добавлять новые файлы в `suppressions.xml` (уже правило п.7 ARCHITECTURE.md).
|
||
Сложность: **L** — трогает ~285 файлов; делать малыми PR по пакетам, `build` зелёный на каждом шаге.
|
||
|
||
> ## ✅ ПРОГРЕСС — Задача 2, шаги (a) + (b) ВЫПОЛНЕНЫ, (c) НАЧАТ (2026-10-06)
|
||
>
|
||
> **(a) Линтеры усилены.** Новый `config/checkstyle/checkstyle.xml` — двухуровневый гейт (ratchet):
|
||
> - `severity="error"` (build падает): все прежние error-правила **плюс пороги подтянуты** — `MethodLength` 80→40,
|
||
> `CyclomaticComplexity` 20→15, `NestedIfDepth` 3→2, `ParameterNumber` 7→5, `ClassDataAbstractionCoupling` 25→10,
|
||
> `ClassFanOutComplexity` 30→12, `MagicNumber` и `FinalLocalVariable` **warning→error** (оба чистые!).
|
||
> Добавлены: `ReturnCount` (max 5), `FinalClass`, `HiddenField`, `VisibilityModifier`, `NPathComplexity` (секция вернулась в явном виде).
|
||
> - `severity="warning"` (build зелёный, но каждое нарушение видно в отчёте) — **бэклог дизайна**, правило:
|
||
> **счётчик обнулился → сразу переводить в error**. Актуальный список — в ПРОГРЕСС-6 ниже
|
||
> (на шаге 0.5: `VisibilityModifier` 163, `ReturnCount` 69, `HiddenField` 30, `IllegalCatch` 23,
|
||
> `ClassFanOutComplexity` 17, `NPathComplexity` 12, `ParameterNumber` 8, `MethodLength` 5, `CDAC` 2).
|
||
> - **`suppressions.xml` ОБНУЛЁН** (45 записей удалено, включая FileLength/NPath/WhitespaceAround/IllegalCatch).
|
||
>
|
||
> **Красный baseline (честный, до фиксов): 431 нарушение в 129 файлах.** Замеры порогов перед выбором:
|
||
> `ReturnCount` max 3→5 (88→69), `NPathComplexity` max 30→50 (36→12) — пороги 3/30 дают мусор на легаси-кодах,
|
||
> реальная работа начинается с 5/50.
|
||
>
|
||
> **(b) Список по пакетам (после фиксов, 329 нарушений / 103 файла / 60 пакетов):**
|
||
>
|
||
> | Пакет | ↑ | | Файл | ↑ |
|
||
> |---|---|---|---|---|
|
||
> | `config/options` | 53 | | `config/options/Option.java` | 45 |
|
||
> | `config/options/section` | 38 | | `config/options/section/OptionGeneration.java` | 25 |
|
||
> | `player/core` | 19 | | `generator/core/model/GeneratorState.java` | 15 |
|
||
> | `generator/core/model` | 17 | | `menu/core/Menus.java` | 15 |
|
||
> | `config/core` | 15 | | `mode/base/Modes.java` | 15 |
|
||
> | `menu/core` | 15 | | `config/core/Config.java` | 14 |
|
||
> | `mode/base` | 15 | | `player/core/ParkourPlayer.java` | 13 |
|
||
> | `elytra/generator` | 12 | | `config/options/OptionGeneral.java` | 8 |
|
||
>
|
||
> Остальные 52 пакета — ≤ 7 нарушений; размазаны по `ReturnCount` (52 файла, ≤ 5 каждая) и `VisibilityModifier` (20 файлов).
|
||
>
|
||
> **(c) Исправлено 102 нарушения (431 → 329):**
|
||
> - `FinalClass` **37** — во всех классах с единственным private-конструктором добавлен `final` (безопасно: такой класс всё равно не наследуется).
|
||
> - `WhitespaceAround` **22** — 11 пустых блоков `{}` → `{ }` (пустые конструкторы, `new TypeToken<…>(){}`, пустой record-body, `default -> {}`).
|
||
>
|
||
> **Состояние на конец шага:** `./gradlew build` — **BUILD SUCCESSFUL**, checkstyle **0 error / 329 warning**, PMD **0**,
|
||
> тесты **94 / 0 fail (1 skipped)**, jar собирается (матрица версий не перепроверялась после правок — только локальная сборка).
|
||
> Изменено 45 файлов (TODO.md, 2 конфига линтеров, 46 исходников).
|
||
>
|
||
> **Осталось (порядок):**
|
||
> 1. ✅ Тест-гейт «**в папке ≤ 4 файлов**» (JUnit, без новых зависимостей) + разбивка 6 переполненных пакетов —
|
||
> сделано в `4263df5`, `FOLDER_DEBT = Set.of()`, ни одна папка `src/main/java` не содержит >4 файлов.
|
||
> 2. ✅ Бэклог `config/options` обнулён (см. ПРОГРЕСС-5 ниже) → **`VisibilityModifier` ещё НЕ в error**: в пакете остались
|
||
> только 2 `IllegalCatch` (общий бэклог п.3), а правило висит на 75 нарушениях в других пакетах.
|
||
> 3. `IllegalCatch` 23: 3 сайта намеренные (команда/конфиг с откатом/скетчи — boundary-catch), остальные 20 — выловить конкретные типы;
|
||
> **нужно решение владельца** по трём.
|
||
> 4. Дальше по плану (a): PMD-категории целиком, CPD, SpotBugs, ArchUnit-слои, JaCoCo, Spotless, quality-job в CI — ✅ сделано в `b771cc1`.
|
||
>
|
||
> ## ✅ ПРОГРЕСС-5 (Задача 2, шаг (c): `config/options` инкапсулирован, 2026-10-06)
|
||
> Был анти-паттерн: `Option` — фасад-зеркало на **45 `public static` полей**, которые `syncFields()` копировал
|
||
> из 4 секций (`OptionSQL`, `OptionParticles`, `OptionGeneration`, `OptionGeneral`), и в самих секциях поля тоже
|
||
> были `public static` (мутабельное состояние чужих пакетов = п.77/107 в миниатюре).
|
||
> - **`Option` сокращён до `init(boolean)`** — единственная точка перезагрузки конфигов (её зовёт `ConfigLoader`);
|
||
> зеркало, `syncFields()` и мёртвый `@Deprecated initStyles` (0 вызовов) удалены. Класс больше не дублирует секции.
|
||
> - **Секции: `private static` поля + fluent-аксессоры** (`OptionSQL.enabled()`, `OptionGeneration.cleanupInterval()`,
|
||
> `OptionGeneral.goBackLocation()`…), без префикса `get` — как у соседних статик-холдеров (`Materials.parse`, `Probs.random`).
|
||
> Две точки записи `Option.SQL = false` → `OptionSQL.disable()`.
|
||
> - **Защита от протечки состояния:** `possibleLeads()/optionsEnabled()/optionsDefaults()` отдают unmodifiable-представления
|
||
> (null сохраняется как раньше), `goBackLocation()` — копию (`Location` мутируется при reload через `setPitch/setYaw`).
|
||
> - `ParticleShape` вынесен из вложенного enum'а в `config/options/ParticleShape.java` (в `section/` леж бы 5-й файл — гейт «≤4»).
|
||
> - **Удалены мёртвые данные:** 17 полей вероятностей генерации + `initGenerationProbabilities()` — не читались нигде,
|
||
> живые веса берутся из `advanced.*` (`GeneratorProfileManager`/`GenerationProfileLoader`). Границы `min-y/max-y`
|
||
> с проверкой `minY >= maxY` перенесены 1-в-1 в `initBounds()`. Найденное оформлено пунктами **164–166**.
|
||
> - **63 обращения `Option.<ПОЛЕ>`** (не 127 — счёт предыдущей сессии был завышен из-за подстроки в `ParkourOption.*`)
|
||
> переписаны в **19 файлах**; `Option`-импорт остался только в `ConfigLoader`.
|
||
> - Попутно (предложения `CodeReview`): `ConfigLoader.validateConfigs()` проверяет **живые** пути
|
||
> `advanced.distance.chances|height.chances|height.min|max|type` (раньше проверялась мёртвая секция `generation.normal.height.*`);
|
||
> в `ParkourRestrictionListener.onChat` unboxing `Boolean` → `getOrDefault(CHAT, true)` как в `ParkourOption.mayPerform`.
|
||
> - **Счётчики:** checkstyle **0 error / 329 → 240 warning**; `VisibilityModifier` **163 → 75**, `MethodLength` 5 → 4.
|
||
> В `config/options` — **0 нарушений VisibilityModifier** (было 88), остаются 2 `IllegalCatch`.
|
||
> `./gradlew check` зелёный: PMD 0, тесты 94/0 fail, JaCoCo/ArchUnit/Spotless ок, NullAway 48 (как в baseline;
|
||
> ⚠️ как выяснилось на шаге 6 — это было артефактом обрезки javac на 100 предупреждениях, честные числа там же).
|
||
> Пороги правил не снижались, `suppressions.xml` пустая. Реальных прогонов на сервере после правок не было (только `check`).
|
||
>
|
||
> ## ✅ ПРОГРЕСС-6 (Задача 2, шаг (c): инкапсуляция игрового состояния — `player/*` + `GeneratorState`, 2026-10-06)
|
||
> **Checkstyle 329 → 165 warning; `VisibilityModifier` 75 → 0 и переведён в `error`.**
|
||
> - **Кластер `player` закрыт:** `ParkourUser` (5 полей) и `ParkourPlayer` (12) → `private` + аксессоры без `get` (та же идиома,
|
||
> что в ПРОГРЕСС-5 для `config/options`). ~46 обращений в 19 файлах переписаны.
|
||
> - **Выключен баг тенимого `locale`:** `@Expose String locale` был объявлен и в `ParkourUser`, и в `ParkourPlayer` —
|
||
> запись в поле и в аксессор расходилась. Осталось одно поле в родителе; на gson-2.11.0 проверено, что `@Expose`
|
||
> наследуемого поля сериализуется, поэтому набор ключей на диске не изменился.
|
||
> - **П.107 для `GeneratorState`:** 15 публичных полей → `private` + аксессоры/сеттеры; инкременты и сбросы оформлены
|
||
> методами (`addGeneratedBlocks(int)`, `resetScore()`, `setScore(int)`), ~90 обращений в 25 файлах.
|
||
> Публичные `ParkourGenerator.session/island/profile/state` — как были (остаток п.107).
|
||
> - **SRP: настройки игрока вынесены в `player/data/PlayerSettings`** (11 полей + аксессоры). `ParkourPlayer` —
|
||
> 194 → 93 строки, засчитываемых методов 20 → 9. Это и починило свежесломанное нарушение PMD `TooManyMethods` (гейт 0 нарушений
|
||
> был испорчен моим же рефакторингом) — декомпозицией, а НЕ ослаблением порога и НЕ переименованием под name-based
|
||
> освобождение правила (`getX()` в проекте не используется: 0 объявлений в `src/main`).
|
||
> - **DRY по настройкам:** троекратное повторение санитизации «legacy-locale» → `PlayerSettingsManager.sanitizeLocale()`
|
||
> (фолбэк берётся из конфига, а не захардкожен `"en"`); байт-в-байт одинаковая сборка 12-ключевого map в
|
||
> `StorageDisk.readPlayer` и `SQLDataMapper.serializeSettings` → `toSettingsMap()`; удалены мёртвый `getColumnMappings()`,
|
||
> `PLAYER_COLUMNS` и дубль `OptionContainer` (п.106); реестр-фасады `ParkourPlayer.isPlayer/getPlayer/getPlayers`
|
||
> переехали в `UserRegistry` (22 файла);
|
||
> дубли `parseIntSafe`/`parseDoubleSafe` схлопнуты в `numericOrFallback`.
|
||
> - **Закрыт п.30** (SQL не сохранял настройки) и **формат disk-файла игрока пересобран сознательно:** раньше Gson писал
|
||
> `ParkourPlayer` по `@Expose`-полям, а читал его через Unsafe-аллокацию «полупустого» объекта, чей map потом переливали
|
||
> в живого игрока. Теперь `StorageDisk` пишет и читает тот же канонический `Map<String,Object>`, что и JSON-колонка SQL —
|
||
> один словарь на оба бэкенда, `@Expose` из доменных классов убран совсем. Старые файлы остаются читаемыми: 3 ключа,
|
||
> которые совпадали с именами полей (`particles`/`useSpecialBlocks`/`showFallMessage`), нормализует
|
||
> `StorageDisk.normalizeLegacyKeys()`. ⚠️ Живых файлов игроков в `run/plugins/LoParkour/players/` нет — миграция
|
||
> проверена кодом и тестами, не реальным апгрейдом.
|
||
> - **Артефакт замера предупреждений:** в `build.gradle.kts` (JavaCompile) добавлен `-Xmaxwarns 100000`, потому что javac по умолчанию
|
||
> обрывает список предупреждений на 100 — отсюда «NullAway 48» в ПРОГРЕСС-5. Честные замеры через `git stash`:
|
||
> baseline HEAD **NullAway 165 / Error Prone 289**, сейчас **NullAway 164 / EP 265** — регресса нет.
|
||
> - **Счётчики:** checkstyle **0 error / 165 warning** (бэклог: `ReturnCount` 69, `HiddenField` 30, `IllegalCatch` 23,
|
||
> `ClassFanOutComplexity` 17, `NPathComplexity` 12, `ParameterNumber` 8, `MethodLength` 4, `CDAC` 2).
|
||
> В ноль обнулены и переведены в `error`: `VisibilityModifier`, `EmptyLineSeparator`, `MissingDeprecated`.
|
||
> PMD **0** (жёсткий гейт), `./gradlew check` зелёный, тесты **94 / 0 fail (1 skipped)**. Пороги правил не снижались,
|
||
> `suppressions.xml` пустая. Реальных прогонов на сервере не было (только `check`).
|
||
> - **Найдено по пути и оформлено отдельными пунктами (молча не чинилось):** **167** (elytra-под-режимы недостижимы —
|
||
> `getMode()` не переопределён) и **168** (`ParkourUser.chatType` не записывается нигде).
|
||
>
|
||
> ## ✅ ПРОГРЕСС-7 (Задача 2, шаг (c): `HiddenField` обнулён + директива владельца про линтеры, 2026-10-06)
|
||
> **Checkstyle 165 → 135 warning; `HiddenField` 30 → 0 и переведён в `error`.**
|
||
> - **Директива владельца (зафиксирована, действует дальше):** нарушения линтеров **чинить**, а не **глушить** —
|
||
> пороги правил не поднимать, `suppressions.xml` не наполнять, под name-based освобождения не подстраиваться.
|
||
> В `error` правило переводится только после того, как счётчик реально обнулён правками.
|
||
> - **`HiddenField` 30 → 0** — тенящие параметры/локалы переименованы (поля не трогали): `Config` (12 методов-делегата,
|
||
> параметр `path` → `key`), `ObstacleGenerator` (внешнее поле `radius` → `obstacleRadius` — одним переименованием сняли
|
||
> 7 теней в enum-реализациях `getPoints(center, radius)`), `GhostService.setReplaying(uuid, active)`,
|
||
> `Leaderboard.sort(order)`, `PagedMenu.addToDisplay(menuItems)`, `Reward` (локал `string` → `command`),
|
||
> `ParticleData` (fluent-сеттеры принимают `value`), `World.create()` (локал `world` → `existingWorld`, а
|
||
> `World.world = world` превратился в обычное присваивание поля).
|
||
> - **`ReturnCount` 69 — решено оформить отдельным пунктом 169:** все 69 нарушений приходятся на половину правила
|
||
> про void (`maxForVoid` по умолчанию = 1, то есть запрещена любая guard-клауза `if (x) { return; }`);
|
||
> для value-методов при `max=5` нарушений **0**. Поднимать `maxForVoid` я предлагал, владелец отказал — значит
|
||
> 69 void-методов переструктурируются по-настоящему (early-return → `if`-блок или выделение helper'а),
|
||
> с оглядкой на `NestedIfDepth` (error, max=2) и `NPathComplexity`.
|
||
> - **`IllegalCatch` 23** — сузить типы там, где конкретика реальна (IO/NumberFormat/SQL/ClassNotFound),
|
||
> а на истинных границах заменить `catch (Throwable)` → `catch (RuntimeException)` (это не «приглушение», а починка:
|
||
> `Throwable` глотает `Error`-ы — OOM, StackOverflow). Диспетчер команд, `onEnable/onDisable`, загрузчики конфигов
|
||
> и схематчей, session-state — пограничные точки, их фиксируем отдельно (п.170).
|
||
> - **Авария и восстановление:** после частичного отката правок в IDE (отклонённые diffs) рабочий код был сломан:
|
||
> `player/data/PlayerSettings.java` удалился целиком, `ParkourPlayer` вернулся к 11 `@Expose`-полям при уже
|
||
> переписанных вызовах (`settings()` ссылался на несуществующее поле, `updateGeneratorSettings` пропал),
|
||
> в `GeneratorCleanup` потерялся импорт `@Nullable` (2 ошибки компиляции), `checkstyle.xml` откатился к
|
||
> HEAD-версии и **потерял все четыре флипа** (`VisibilityModifier`, `EmptyLineSeparator`, `MissingDeprecated`,
|
||
> `HiddenField`), `PagedMenu`/`GhostService` вернулись к тенящим параметрам, а с `ParkourUser` слез лишний `@Expose`
|
||
> (после перехода диска на map он мёртвый). Всё восстановлено и перепроверено:
|
||
> `./gradlew check` — **BUILD SUCCESSFUL**, checkstyle **0 error / 135 warning** (test-отчёт пуст), PMD **0**,
|
||
> тесты **94 / 0 fail (1 skipped)**, `suppressions.xml` пустая. Реальных прогонов на сервере по-прежнему не было.
|
||
> Урок на будущее: после любой отменённой пачки правок сверять `git status` + полный `check`, а не только счётчики.
|
||
> - **Остаток бэклога (135):** `ReturnCount` 69, `IllegalCatch` 23, `ClassFanOutComplexity` 17, `NPathComplexity` 12,
|
||
> `ParameterNumber` 8, `MethodLength` 4, `ClassDataAbstractionCoupling` 2.
|
||
>
|
||
> ## ✅ ПРОГРЕСС-8 (Задача 2: починены реальные баги 167 и 168, 2026-10-06)
|
||
> По решению владельца вместо механической зачистки варнингов взяты два мёртвых куска логики.
|
||
> - **167 — elytra-под-режимы научились представляться.** `ElytraGenerator` больше не возвращает хардкод
|
||
> `Modes.elytra()`: режим передаётся в конструктор (`private final Mode mode`, канонический конструктор
|
||
> `(session, pointType, mode)` + two convenience-перегрузки), `getMode()` отдаёт его. Под-генераторы
|
||
> объявляют свою идентичность одной строкой `super(session, Modes.speedDemon())` — вместо пяти копий `getMode()`
|
||
> (у `ObstacleGenerator` не хватило бы строк: лимит FileLength = 200, а файл — 199).
|
||
> Попутно удалено дублирование: `ObstacleGenerator` заново читал `elytra.radius` и держал свой `DEFAULT_RADIUS = 3` —
|
||
> теперь общий `radius()` из родителя (199 → 194 строки).
|
||
> - **Что реально поменялось для игрока (следствие, а не побочка):**
|
||
> 1) guard в `create()` стал достижим → повторная `/parkour speeddemon` в том же режиме больше не пересоздаёт сессию;
|
||
> 2) обратная сторона — `/parkour elytra` внутри под-режима раньше **не работал** (под-генератор выдавал себя за
|
||
> `ElytraMode`, `instanceof ElytraMode` был истинен и делал early-return), теперь переключает;
|
||
> 3) очки, rewards (`%mode:`-префикс в `rewards-v2.yml`), голограммы и PAPI теперь попадают в **свой** лидерборд
|
||
> под-режима, а не в общий `elytra`. ⚠️ Старые записи в `leaderboards/elytra.json`/SQL не теряются, но
|
||
> пер-режимные доски начинаются с нуля; реальных прогонов на сервере не было (только `check`).
|
||
> - **168 — переключатель области чата подключён к меню.** Фича была спроектирована, но не собрана: блоки
|
||
> `settings.chat` (material/name/lore/values + formats) есть и в `en.yml`, и в `ru.yml`, `ParkourOption.CHAT` зарегистрирован,
|
||
> `ParkourRestrictionListener.onChat` уже умеет маршрутизировать `LOBBY_ONLY`/`PLAYERS_ONLY`, но кнопка не
|
||
> создавалась, сеттера не было, и `chatType` навечно оставался `PUBLIC`.
|
||
> Сделано: `Session.ChatType.next()` (цикл по порядку объявления = порядку `settings.chat.values`),
|
||
> `ParkourUser.setChatType(...)`, кнопка в `SettingsMenu` (слот 15, gate `ParkourOption.CHAT.mayPerform`) по образцу
|
||
> `LobbyMenu.visibilityItem` — клик меняет область и перерисовывает меню. Новатье — `ChatTypeTest` (2 теста).
|
||
> - **Счётчики:** checkstyle **0 error / 135 warning** (тот же бэклог, что в ПРОГРЕСС-7 — баги чинились без
|
||
> новых варнингов), PMD **0**, тесты **96 / 0 fail (1 skipped)**, `./gradlew check` зелёный.
|
||
|
||
---
|
||
|
||
## 🗺 План: поддержка версий 1.19.2 → 26.3 (Java 21 + Java 25), смена команды
|
||
|
||
> Задано владельцем 2026-10-06. Сейчас: `build.gradle.kts` параметризован (`-PmcVersion/-PapiCoordinate/-PjavaRelease/-PapiVersion`),
|
||
> CI-матрица собирает 1.19.2 / 1.20.1 / 1.21 / 1.21.11 (Java 21) и 26.1.2 / 26.2 (Java 25, `release: 25`). **Только сборка — запуск на серверах не проверяется.**
|
||
|
||
- **139. [P1] Целевой диапазон: Paper/Spigot 1.19.2 … 26.3 и Java 21 + Java 25.** MC 26.x требует Java 25 на сервере, поэтому плагин должен работать на JDK 21 *и* 25.
|
||
Ориентир — IP-Reborn: один jar с байткодом Java 21 (`release 21`), собранный на JDK 25, запускается на Java 21 и 25 (`ref/IP-Reborn/README.md`, раздел Building).
|
||
→ Не выпускать отдельные jar под `release 25` (сломают 1.21.x на Java 21); оставить `release 21` для всех. Проверить, что `lolib-3.0.0.jar` (байткод 21) и shadow-релокации (ASM) читают class-version 65 на JDK 25.
|
||
→ Если нужен запуск 1.19.2–1.20.4 на Java 17 — отдельное решение: lolib и наш код пересобрать на `release 17` (иначе нижняя граница фактически Java 21).
|
||
- **140. [P1] Добавить 26.3 в CI-матрицу и в README.** IP-Reborn проверяет `26.3.build.142-beta`; у нас только 26.1.2/26.2. Обновить бейджи/раздел «Supported servers» (сейчас «Paper 1.19.2+»).
|
||
- **141. [P1] Запуск, а не только сборка.** CI собирает jar, но не стартует сервер. → smoke-тест: поднять Paper на каждой версии (`run-paper` уже подключён), проверить, что плагин включается без исключений. Начать с 1.19.2, 1.21.11, 26.3.
|
||
- **142. [P1] Убрать API, которых нет в старых/новых версиях.** Известно: `GameRule.*` и `Particle.*` разные (см. п.129: `Particle.FIREWORK` нет в 1.20.4), `Material.STONE_SLAB` (п.100), `BukkitRunnable.runTaskTimerAsynchronously` (п.21), `registerNewObjective(String,String,String)`. В 5.4.0-paper26 IP-Reborn заменял «removed Paper and Adventure API calls» — просмотреть их список как чек-лист.
|
||
→ Вынести версионно-зависимое в один слой `compat/` (определение версии по `Bukkit.getVersion()` — как `ref/ajParkour/.../utils/VersionSupport.java`; для 26+ учесть смену схемы версий: major ≥ 26).
|
||
- **143. [P1] Folia (п.21, issue #1).** `World.create()` уже не вызывает `createWorld` для загруженного мира (коммит «reuse already loaded world…»). Остаётся: `folia-plugin.yml`/`folia-supported: true`, region-scheduler вместо BukkitRunnable, настройки мира из global region. IP-Reborn сознательно *не* поддерживает Folia — честнее убрать бейдж, если не успеваем.
|
||
- **144. [P1] Заменить команду `/lp` — конфликт с LuckPerms.** LuckPerms использует `/lp`; у нас `plugin.yml`: `loparkour` с `aliases: [parkour, lp]`. Оба плагина борются за алиас, при загрузке в разном порядке один из них теряет команду. Заодно `parkour` конфликтует с плагином A5H73Y Parkour и с IP.
|
||
→ Убрать `lp` (и, вероятно, `parkour`) из `aliases`; оставить `loparkour` + короткий алиас без конфликтов (предложение: **`lpk`**; *имя ждёт решения владельца*).
|
||
→ Править: `src/main/resources/plugin.yml`; все `/lp …` в `README.md` (15), `docs/SCHEMATICS.md` (16), `src/main/resources/docs/SCHEMATICS.md`, `schematics/schematics.yml`, `docs/ARCHITECTURE.md`, локали, тексты сообщений/usage; `ReplayCommandExecutor` (javadoc `/lp replay`).
|
||
→ Права (`loparkour.*`) не менять. Добавить в CHANGELOG пометку о breaking-изменении алиаса; рассмотреть deprecation-сообщение при вызове старого алиаса, если он останется.
|
||
|
||
## 📥 Что взять из `ref/` (IP-Reborn, ajParkour, A5H73Y Parkour)
|
||
|
||
> Берём только **идеи**: всё реализуем своим кодом, чужой код в репозиторий не копируем (поэтому атрибуции/NOTICE не нужны).
|
||
> Список — кандидаты по структуре и README; каждый пункт перед реализацией проверить по коду.
|
||
|
||
### IP-Reborn (`ref/IP-Reborn`)
|
||
|
||
- **145. Native void-генератор** (`world/`, в 6.0 убрали зависимость от VoidGen/PaperLib/vilib). У нас внешний `dev.lolib.worlds.VoidGenerator` и `softdepend: VoidGen` — закрывает п.112.
|
||
- **146. Паритет режимов** (`ipp/mode/single`, `ipp/mode/multi`, `ipp/mode/lobby`): Practice, Hourglass, SuperJump, TimeTrial, WaveTrial, Speed, TeamSurvival, Lobby/LobbySelection — основа для п.110/111 (invite-лобби).
|
||
- **147. `HologramManager`/`TrackedHologram`/`HologramConfig`** (`hologram/`) — заменить хрупкий `HoloHook` (п.87).
|
||
- **148. `LegacyDataMigrator` + `MIGRATION.md`** — шаблон безопасной миграции данных старых версий без удаления старых папок; полезно для смены команды/структуры конфигов.
|
||
- **149. Жизненный цикл интеграций** (`integration/IntegrationLoader`): снятие PAPI-expansion и хуков при reload/disable; по одному `HandlerList` на каждое кастомное событие — проверить наши `api/event/*`.
|
||
- **150. Документация-сайт** (`docs/guide`, `docs/index.md`, zh-перевод) — шаблон для п.114.
|
||
- **151. Правила сборки/CI под 26.x** (`pom.xml`, `CHANGES.md` 5.4.0-paper26/6.0.x) — как держат один jar на 1.21.11…26.3.
|
||
|
||
### A5H73Y Parkour (`ref/Parkour`, MIT)
|
||
|
||
- **152. Versioned upgrade системы** (`upgrade/major/*`) — пошаговые миграции конфигов/данных по версиям; решение для п.26 (ConfigUpdater) и п.62 (SQL-мигратор).
|
||
- **153. `PluginBackupUtil`** — автобэкап конфигов/данных перед апгрейдом.
|
||
- **154. Реестр команд** (`commands/CommandUsage`, `ParkourAutoTabCompleter`, `utility/parkourCommands.json`): описание команд в данных → одновременно help, tab-complete и права; убрало бы ручной `CommandTabCompleter`.
|
||
- **155. Чекпоинты / киты / челленджи / звуки** (`type/checkpoint`, `type/kit`, `type/challenge`, `type/sounds`) — идеи фич для карт; `TaskCooldowns`, `SignUtils` (таблички) — мелкие утилиты.
|
||
- **156. Conversation-API для настройки через чат** (`conversation/*`) — альтернатива меню для ввода значений (приз, режим).
|
||
- **157. Плагин-обёртки** (`plugin/PluginWrapper`, `EconomyApi`, `PermissionVault`, `BountifulApi`, `PlaceholderApi`) — единый паттерн ленивого подключения soft-зависимостей; решает п.89 (VaultHook кэширует навсегда).
|
||
- **158. `parkourPlaceholders.json`** — список плейсхолдеров данными; идея для п.120.
|
||
|
||
### ajParkour (`ref/ajParkour`, GPL-3.0)
|
||
|
||
- **159. `VersionSupport`** (версия по `Bukkit.getVersion()`, учёт major ≥ 26, action bar через spigot API) — готовая основа для п.142.
|
||
- **160. Портал/зона** (`Portal`, `AreaStorage`, `game/PkArea`, `WorldeditSelector`) — вход в паркур через зону/портал в мире, выбор области WorldEdit.
|
||
- **161. `TopManager`/`TopEntry`** — компактный top-10 для голограмм/PAPI.
|
||
- **162. `Updater`** — проверка новой версии (opt-in, без блокировки main thread).
|
||
- **163. `jumps.yml` + `Difficulty`** — выносить параметры прыжков в данные (закрывает мёртвый `jump-validation`/`jump-types`, п.38).
|
||
|
||
---
|
||
|
||
## Итог по аудиту
|
||
|
||
- Найдено: **~125 новых пунктов** (P1: 18, P2: 21, P3: 28, P4: 14, тесты: 2, конкуренты: 13, наследие форка: 5, новые режимы: 10, зачистка варнингов: 7).
|
||
- Мёртвые фичи: ghost (п.22), coop (п.23-24), jump-types/EffectManager (п.38).
|
||
- Мёртвый конфиг: `jump-validation`, `jump-types`, `ghost-mode`, `useSpecialBlocks`, `blockLead`.
|
||
- Главные риски производства: Folia-краш, утечка SQL-пулов, потеря данных в ConfigUpdater, креш от битых схем.
|