- 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.
94 KiB
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использует удалённое APIBukkitRunnable.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(switchchatType()), но сеттера нет нигде → ветки 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) его не переопределяют → guardgetMode() 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.
SQLDataMapperUUID.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. В shippedgeneration.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-playerParkourPlayer.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;updateVisibilityO(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.getModecase-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.localederefOptionGeneral.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/настройки),PreviousDatarestore. - 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из командного блока), у нас нет. → ПоддержатьCommandBlocksender в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.ymlmodes.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()с guardstopped;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}+ configmode-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 (пороги подняты под реальность — ВСЁ чисто)
ClassFanOutComplexitymax 10→20,ClassDataAbstractionCouplingmax 7→20 (checkstyle, остаются warning-подсказками);TooManyMethodsmaxMethods 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, автосервисы) |
Шаги:
- Выяснить фактическую поверхность API:
javap/декомпилятор по 5 классам, список методов, реально вызываемых из нашего кода (grepпоdev.lolib— всё остальное внутри jar нам не нужно). - Транзитивные библиотеки: lolib — fat-jar (gson, HikariCP, caffeine, snakeyaml, slf4j, checkerframework). Сейчас gson/Hikari у нас и так объявлены явно (
FileStatsStorage,StorageDisk,SQLDataMapper,SQLConnectionManager,ParkourPlayer); caffeine/snakeyaml/slf4j напрямую не импортируются — убедиться, что без jar сборка и тесты проходят, не добавлять лишнего. - Написать замены (scheduler → void-gen → gui →
JavaPlugin), по одной, с тестами; после каждой —./gradlew build. - Удалить
libs/lolib-3.0.0.jar,implementation(files(...)), релокациюdev.lolibи закомментированныйexcludeвshadowJar; убратьsoftdepend: VoidGenизplugin.yml. - Проверка: jar без
dev/loki/loparkour/lib/lolib/**(unzip -l), плагин стартует на 1.21.x/26.x (см. п.141), размер jar заметно меньше. - Папка
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 строк на файл — гейт уже есть (
FileLength200, error); проверить, что супрессий на FileLength не осталось; снизитьMethodLength80 → ~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),ParameterNumber7 → 5,ReturnCount,NestedIfDepth2 → 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уже стоит — оставить. - Новые проверки (предложение, выбрать):
- ArchUnit (тест в JUnit): слои, отсутствие циклов между пакетами, «в папке ≤ 4 файла» и «≤ 200 строк» как тест, запрет
BukkitRunnable/Bukkit.getScheduler()внеscheduler/(п.109 — Folia-инварианты), запретorg.bukkitвapi/core. - SpotBugs (+
find-sec-bugs) — NPE/гонки/SQL-инъекции, закрывает целый класс пунктов P2 по потокобезопасности. - Error Prone + NullAway (compile-time) — null-safety на всём коде,
@Nullableна границах. - PMD CPD — гейт на дубли (DRY).
- JaCoCo — минимальное покрытие (старт 50% строк, ядро — 70%), отчёт в CI; п.108 (тесты) становится обязательным.
- Spotless (google-java-format/palantir) — единый формат,
spotlessCheckв CI; убираетWhitespaceAround/EmptyLineSeparatorшум. - OWASP dependency-check или
gradle-versions/dependabot— уязвимости/устаревшие зависимости (HikariCP 5.1.0, mysql-connector 8.3.0, bstats). - forbidden-apis — запрет deprecated/запрещённых вызовов (
String.formatбез Locale,Thread.sleep,System.out). - Gradle
dependency-analysis— лишние/недостающие зависимости (поймает остатки lolib). - Проверка дат/версий: тест, что
plugin.yml/build.gradle.kts/README/CHANGELOG версии совпадают.
- ArchUnit (тест в JUnit): слои, отсутствие циклов между пакетами, «в папке ≤ 4 файла» и «≤ 200 строк» как тест, запрет
- 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-правила плюс пороги подтянуты —MethodLength80→40,CyclomaticComplexity20→15,NestedIfDepth3→2,ParameterNumber7→5,ClassDataAbstractionCoupling25→10,ClassFanOutComplexity30→12,MagicNumberиFinalLocalVariablewarning→error (оба чистые!). Добавлены:ReturnCount(max 5),FinalClass,HiddenField,VisibilityModifier,NPathComplexity(секция вернулась в явном виде).severity="warning"(build зелёный, но каждое нарушение видно в отчёте) — бэклог дизайна, правило: счётчик обнулился → сразу переводить в error. Актуальный список — в ПРОГРЕСС-6 ниже (на шаге 0.5:VisibilityModifier163,ReturnCount69,HiddenField30,IllegalCatch23,ClassFanOutComplexity17,NPathComplexity12,ParameterNumber8,MethodLength5,CDAC2).suppressions.xmlОБНУЛЁН (45 записей удалено, включая FileLength/NPath/WhitespaceAround/IllegalCatch).Красный baseline (честный, до фиксов): 431 нарушение в 129 файлах. Замеры порогов перед выбором:
ReturnCountmax 3→5 (88→69),NPathComplexitymax 30→50 (36→12) — пороги 3/30 дают мусор на легаси-кодах, реальная работа начинается с 5/50.(b) Список по пакетам (после фиксов, 329 нарушений / 103 файла / 60 пакетов):
Пакет ↑ Файл ↑ config/options53 config/options/Option.java45 config/options/section38 config/options/section/OptionGeneration.java25 player/core19 generator/core/model/GeneratorState.java15 generator/core/model17 menu/core/Menus.java15 config/core15 mode/base/Modes.java15 menu/core15 config/core/Config.java14 mode/base15 player/core/ParkourPlayer.java13 elytra/generator12 config/options/OptionGeneral.java8 Остальные 52 пакета — ≤ 7 нарушений; размазаны по
ReturnCount(52 файла, ≤ 5 каждая) иVisibilityModifier(20 файлов).(c) Исправлено 102 нарушения (431 → 329):
FinalClass37 — во всех классах с единственным private-конструктором добавленfinal(безопасно: такой класс всё равно не наследуется).WhitespaceAround22 — 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 исходников).Осталось (порядок):
- ✅ Тест-гейт «в папке ≤ 4 файлов» (JUnit, без новых зависимостей) + разбивка 6 переполненных пакетов — сделано в
4263df5,FOLDER_DEBT = Set.of(), ни одна папкаsrc/main/javaне содержит >4 файлов.- ✅ Бэклог
config/optionsобнулён (см. ПРОГРЕСС-5 ниже) →VisibilityModifierещё НЕ в error: в пакете остались только 2IllegalCatch(общий бэклог п.3), а правило висит на 75 нарушениях в других пакетах.IllegalCatch23: 3 сайта намеренные (команда/конфиг с откатом/скетчи — boundary-catch), остальные 20 — выловить конкретные типы; нужно решение владельца по трём.- Дальше по плану (a): PMD-категории целиком, CPD, SpotBugs, ArchUnit-слои, JaCoCo, Spotless, quality-job в CI — ✅ сделано в
b771cc1.✅ ПРОГРЕСС-5 (Задача 2, шаг (c):
config/optionsинкапсулирован, 2026-10-06)Был анти-паттерн:
Option— фасад-зеркало на 45public 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.onChatunboxingBoolean→getOrDefault(CHAT, true)как вParkourOption.mayPerform.- Счётчики: checkstyle 0 error / 329 → 240 warning;
VisibilityModifier163 → 75,MethodLength5 → 4. Вconfig/options— 0 нарушений VisibilityModifier (было 88), остаются 2IllegalCatch../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;
VisibilityModifier75 → 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. Это и починило свежесломанное нарушение PMDTooManyMethods(гейт 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 (бэклог:
ReturnCount69,HiddenField30,IllegalCatch23,ClassFanOutComplexity17,NPathComplexity12,ParameterNumber8,MethodLength4,CDAC2). В ноль обнулены и переведены в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;
HiddenField30 → 0 и переведён вerror.
- Директива владельца (зафиксирована, действует дальше): нарушения линтеров чинить, а не глушить — пороги правил не поднимать,
suppressions.xmlне наполнять, под name-based освобождения не подстраиваться. Вerrorправило переводится только после того, как счётчик реально обнулён правками.HiddenField30 → 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превратился в обычное присваивание поля).ReturnCount69 — решено оформить отдельным пунктом 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.IllegalCatch23 — сузить типы там, где конкретика реальна (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):
ReturnCount69,IllegalCatch23,ClassFanOutComplexity17,NPathComplexity12,ParameterNumber8,MethodLength4,ClassDataAbstractionCoupling2.
🗺 План: поддержка версий 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.md5.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, креш от битых схем.