LoParkour/TODO.md
loki5512344 81788e71ed
refactor: decompose god-classes, add lint gates, restructure packages
- Decompose all 11 god-classes (>200 lines) into facade + collaborators
  (command/, schematic/, config/, generator/, mode/, papi/, adaptive/,
  storage/, player/, leaderboard/) — every file is now <=200 lines
- Add PMD 7 (report-only) + stricter checkstyle rules (NPath, IllegalCatch,
  NestedDepth, Whitespace, MissingOverride); fix real bugs found
  (printStackTrace, resource leaks in ConfigLoader/SQLStatsStorage)
- docs/ARCHITECTURE.md: target structure + 'one folder = one purpose' rule
- Pilot restructure: command/core|util/, mode/impl/solo|multi|barrier/,
  hook/papi/resolver/; cross-package classes made public
- TODO.md: track warning cleanup (132-138) + restructure progress
2026-08-05 18:33:21 +02:00

293 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` всегда `"{}"`, десериализация no-op (SQLDataMapper.java:105-115) → при входе `schematicDifficulty`/`blockLead` и др. = null → NPE в `ParkourPlayer.updateGeneratorSettings:157-164` и `PAPIHook.java:140`. → Реализовать сериализацию/десериализацию всех настроек (JSON-колонка) + fallback на defaults.
- **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.
### Modes
- **50. `GravityShiftMode` не снимает эффекты при выходе игрока** (только в reset для присутствующих, GravityShiftMode.java:149-163). → Снимать в unregister.
- **51. `RaceMode.finishRace` — `reset(false)` оставляет игрока в воздухе** (RaceMode.java:142-147; GeneratorCleanup.reset чистит курс без регенерации/телепорта). → Телепорт в лобби + регенерация старта.
- **52. `SpeedrunMode` оставляет удалённые блоки в `history`** → ломает координатные поиски. → Удалять координату из history при снятии блока.
### Схематики (жизнь в игре)
- **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 копиях; префикс `SQL_PREFIX` применён непоследовательно. → Единый мигратор с версионированием; починить/удалить SQL-файл.
- **63. `writeScores` гоняет всю таблицу туда-сюда** без дельт (StorageSQL.java:105-128). → Инкрементальные upsert'ы только изменённых записей.
- **64. `Score.fromString` — CSV по запятой: ник с `,` ломает все поля** (Score.java:19-31). → Экранирование или JSON.
- **65. `validateConnection` мутирует `Option.SQL=false` при транзиентном сбое** (SQLConnectionManager.java:81-86) — 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`, `BORDER_SIZE` устаревший, миры/генераторы не обновляются (ConfigUpdater, OptionStyles.java:23-43, Rewards.java:41-52, OptionGeneral.java:38-40). → Полный 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). → Строгая валидация ключей при старте.
### 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 `OPTIONS_DEFAULTS.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 публичных полей, `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` полностью, оставить только `.schem` (WorldEdit) + `.nbt`.** Выпилить: пакет `schematic/legacy/lpschem/`, `LpschemConverter`, конвертер-команду, `LPSchematicLegacy`, примеры `*.lpschem` из `resources/schematics/`, все ветки `.lpschem` в `SchematicManager`/`SchematicCreator`. Формат устарел, это двойная поддержка (аудита: п.31, 53-56, 94-97) — один формат схем, один путь пасты/валидации.
- **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 — модель «генератор на игрока».** Сейчас наш `ParkourGenerator` = один скорер на сессию (общий `GeneratorState`/history/score, `PlayerInteractionHandler.checkAllPlayers` скорит всех по одной трассе) — это несовместимо с дуэлью. Схема IP-Reborn: `DuelsGenerator` (владелец сессии) держит `Map<ParkourPlayer, SingleDuelsGenerator>`, каждый `SingleDuelsGenerator extends ParkourGenerator` со своим state/history/time/цветом/спавн-островом, разнесены по Z. Победа = первый набравший `goal`. Результаты НЕ пишутся в лидерборд (`getLeaderboard()=null`). Тик идёт только через владельца.
- **123. Duels — изменения ядра:** ослабить `final class Island` (или строить арены вручную), дать хук `selectBlockData()` (фиксированный цвет на игрока), пер-игроковая спавн-площадка со сдвигом.
- **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:2,target-score,island-distance}` в config.yml, локали `modes.duels.*`/`play.multi.*`, `MultiplayerMenu` (фильтр `instanceof MultiMode`) + кнопка в PlayMenu, баннер-старт в hotbar, `/lp create|join <player>|invite`. Сложность: **L**.
- **126. Duels — не повторить баги:** Race (TIME-лидерборд, ресет в воздухе через `runLater`) — у Duels лидерборда нет, время брать из `state.start` своим форматтером, ресет только синхронно с null-guard; Coop (рекурсия, невозможность создать) — не вызывать `ParkourUser.leave` поверх unregister, сессию создавать в `create()`.
### Elytra (порт из `dev.efnilite.iep`, Kotlin → Java)
- **127. Elytra — модель.** Один генератор на игрока (не сессия на многих). Трасса = секция из 5 узлов (`KnotDirector`: dx~N(75,15), dy bias −15/+15 (ASCEND при Y<50), dz~N(0,35)), узлы → кубический сплайн → точки → «кольца» (Брезенхэм, `PointType.CIRCLE(-1)` / `FLAT(-5)` для CloseMode). Очки = пройденное расстояние: каждый тик `movementScore += velocity.x` при `isGliding && x > blockSpawn.x`. Фолл: Y<спавн−radius | дальше radius от точек | не глидишь. Подъёмная секция = фейерверк-буст (`velocity = v*0.7+dir*0.6`, насильный `isGliding`).
- **128. Elytra — клиент-сайд рендер.** `ClientBlockChanger` НЕ ставит блоки в мир: очередь `Map<chunkX, Set<Vector>>`, каждый тик шлёт игроку `player.sendBlockChanges(states)` (MultiBlockChange) только для чанков вперёд по X (limit min(viewDistance,8)). Пакет `ElytraBlockChanger` на Java.
- **129. Elytra — классы:** `ElytraSection`+`ElytraKnotDirector`+`ElytraPointType`, `ElytraGenerator extends ParkourGenerator` (СВОЙ `tick()`, переопределённый `getMode()`, свой фолл-детект — не полагаться на `LifecycleTickManager.checkPlayerFall`), режимы `ElytraMode/Default/SpeedDemon/MinSpeed/TimeTrial/Close/Obstacle` (по одному подклассу генератора), раздача элитр в слот нагрудника + `ParkourHotbar`, `PreviousData` переиспользуем.
- **130. Elytra — зависимости/мир.** Добавить Apache Commons Math (`SplineInterpolator`) или заменить на Catmull-Rom. Отдельный мир не нужен: труба клиент-сайд, наш мир void; подобрать высоту/порог ASCEND под наши zone-границы. Скор в double (наши `Score.score` int — масштабировать или добавить float-вариант). Сложность: **S** базовый, **M** режимы+бусты, **L** Obstacle+настройки+стили+награды.
- **131. Тесты под новые режимы:** сплайн/секции (изоляция, Y/Z интерполяция), `ElytraGenerator.tick` (score по velocity.x, shouldReset-причины), `DuelsGenerator.win` (guard `stopped`, идемпотентность `reset(false)`), `SingleDuelsGenerator.score`→win при goal, рекурсия unregister (регрессия Coop).
---
## 🧹 Зачистка варнингов линтеров (добавлено после ужесточения конфигов)
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 — в силе.
>
> ## ✅ ПРОГРЕСС-2 (правило «одна папка = одна цель» — пилоты применены)
> Правило добавлено в `docs/ARCHITECTURE.md` (п.8 «Принципы проектирования»). Пилоты: `command/`
> → core/ + util/ (player/ уже был), `mode/impl/` → solo/ + multi/ + barrier/, `hook/papi/` → resolver/.
> Классы, пересекающие пакеты, стали public (сигнатуры не менялись). Остальные переполненные папки
> (storage/sql, adaptive/core, api/event, menu/core, config/core|options, schematic/legacy/lpschem,
> player/service, generator/*, util/gui и др.) — кандидаты на аналогичное разбиение по под-целям.
- **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-блок).
После зачистки п.132–137 — удалить соответствующие записи из `config/checkstyle/suppressions.xml` (супрессии остаются только для ещё незачищенных legacy-файлов).
---
## Итог по аудиту
- Найдено: **~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, креш от битых схем.