LoParkour/TODO.md
loki5512344 02e2947f47
refactor(player): encapsulate game state behind PlayerSettings, clear VisibilityModifier and HiddenField
- ParkourUser/ParkourPlayer/GeneratorState public mutable fields -> private + short accessors
- per-player settings extracted to player/data/PlayerSettings (ParkourPlayer 188 -> 96 lines,
  PMD-counted methods 20 -> 9); this is what fixed the TooManyMethods violation that the
  encapsulation itself introduced — by decomposition, not by lowering the threshold
- disk storage reads and writes the same canonical 12-key map as the SQL JSON column;
  legacy key names normalized on read, @Expose dropped from domain classes
- 39 settings call sites rewritten across 7 files
- HiddenField backlog cleared (Config, ObstacleGenerator, GhostService, Leaderboard,
  PagedMenu, Reward, ParticleData, World)
- checkstyle ratchet: VisibilityModifier, HiddenField, EmptyLineSeparator and
  MissingDeprecated flipped to error once their counters hit zero; suppressions.xml empty
- javac -Xmaxwarns raised so NullAway/Error Prone counts are honest
  (baseline 165/289, now 164/265)

checkstyle 0 errors / 135 warnings, PMD 0 violations, tests 94/0 fail, ./gradlew check green.
TODO.md: items 30 and 107 updated, new items 167-170, ПРОГРЕСС-6/7 recorded.
2026-10-06 18:57:39 +02:00

94 KiB
Raw Blame History

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 в m601000+... (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. ParkourUser.chatType — состояние без записи. Поле инициализировано PUBLIC, читается в ParkourRestrictionListener.onChat (switch chatType()), но сеттера нет нигде → ветки PRIVATE/GUILD недостижимы, режим группового чата не работает. → Либо подключить переключатель (команда/меню), либо убрать switch и мёртвый enum.

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. Elytra-под-режимы не отличаются от базового. ElytraGenerator.getMode() возвращает Modes.elytra(), а 5 под-генераторов (Close/MinSpeed/TimeTrial/SpeedDemon/ObstacleGenerator) его не переопределяют → guard getMode() instanceof CloseMode в create() (строка 47 во всех 5 режимах) никогда не истинен: повторная команда в том же режиме не делает early-return, а пересоздаёт сессию. Геттеры Modes.speedDemon()/minSpeed()/timeTrial()/close()/obstacle() не вызываются нигде (5 статиков только пишутся). → Переопределить 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 (разовый толчок v0.7+dir0.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.

🗺 План: поддержка версий 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, креш от битых схем.