From b6712391fdbc42367c6402948516a8588562beb0 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sat, 10 Oct 2026 14:52:10 +0200 Subject: [PATCH] fix(lint,command,scheduler): lpk alias, ReturnCount/IllegalCatch triage, scheduler facade - command: replace /lp alias with /lpk (LuckPerms conflict, p.144); parkour alias kept; docs/changelog/javadoc updated - lint p.169: restructure last 69 void early-returns into if-blocks and helpers; ReturnCount is now an error gate (0 violations) - lint p.170: Throwable->RuntimeException on 3 boundaries; narrow 5 catches to concrete types; document 14 intentional boundary catches - scheduler p.21: add runTimerAsync to the facade (Bukkit + Folia backends); MetricsCacheStore and duels go through the facade, no BukkitRunnable/Bukkit.getScheduler outside scheduler/ (archunit freeze-store emptied); drop the unverified Folia badge from README - tracker: mark p.26-29/32-33 done (fixed in fea15e6, verified) --- CHANGELOG.md | 8 +- README.md | 31 +++-- TODO.md | 52 +++++--- config/checkstyle/checkstyle.xml | 2 +- docs/SCHEMATICS.md | 32 ++--- .../java/dev/loki/loparkour/LoParkour.java | 3 +- .../core/metrics/MetricsCacheStore.java | 15 +-- .../loparkour/bootstrap/PluginBootstrap.java | 1 + .../command/admin/ops/ForceJoinExecutor.java | 8 +- .../command/admin/ops/ForceLeaveExecutor.java | 14 +- .../admin/ops/RecoverInventoryExecutor.java | 4 +- .../command/admin/ops/ResetExecutor.java | 4 +- .../loparkour/command/core/CommandRouter.java | 35 ++--- .../command/core/LoParkourCommand.java | 2 +- .../command/player/JoinCommandExecutor.java | 14 +- .../command/player/PlayerCommandHandler.java | 32 ++--- .../command/player/ReplayCommandExecutor.java | 16 ++- .../command/schematic/SchematicHandler.java | 29 +++-- .../loparkour/config/core/ConfigLoader.java | 1 + .../config/core/merge/ConfigUpdater.java | 2 +- .../loparkour/config/locale/LocaleCache.java | 1 + .../loparkour/config/locale/LocaleLoader.java | 1 + .../config/options/OptionGeneral.java | 4 +- .../loki/loparkour/duels/DuelsCountdown.java | 26 ++-- .../loki/loparkour/duels/DuelsGenerator.java | 68 +++++----- .../loparkour/elytra/close/CloseMode.java | 9 +- .../loparkour/elytra/core/ElytraBoost.java | 22 ++-- .../elytra/core/ElytraGenerator.java | 35 ++--- .../loparkour/elytra/core/ElytraMode.java | 9 +- .../elytra/minspeed/MinSpeedMode.java | 9 +- .../elytra/obstacle/ObstacleGenerator.java | 10 +- .../elytra/obstacle/ObstacleMode.java | 9 +- .../elytra/speeddemon/SpeedDemonMode.java | 9 +- .../elytra/timetrial/TimeTrialMode.java | 9 +- .../coordinator/GeneratorProfileManager.java | 4 +- .../jump/calculation/JumpValidator.java | 53 +++----- .../generator/jump/placement/BlockPlacer.java | 33 ++--- .../lifecycle/loop/LifecycleEventManager.java | 17 +-- .../lifecycle/player/GeneratorCleanup.java | 16 +-- .../loparkour/ghost/core/GhostManager.java | 3 +- .../loparkour/ghost/core/GhostRecorder.java | 13 +- .../loparkour/ghost/service/GhostReplay.java | 34 ++--- .../loki/loparkour/hook/holo/HoloHook.java | 77 ++++++----- .../resolver/GlobalPlaceholderResolver.java | 34 ++--- .../resolver/PlayerPlaceholderResolver.java | 59 +++++---- .../loki/loparkour/hook/vault/VaultHook.java | 22 ++-- .../gameplay/ParkourRestrictionListener.java | 122 ++++++++---------- .../player/PlayerConnectionListener.java | 22 ++-- .../schematic/SchematicWandListener.java | 15 +-- .../menu/community/SingleLeaderboardMenu.java | 19 ++- .../menu/gui/inventory/GuiManager.java | 24 ++-- .../loki/loparkour/menu/lobby/DuelsMenu.java | 17 +-- .../loparkour/menu/play/SpectatorMenu.java | 17 +-- .../impl/barrier/InvisibleBarrierMode.java | 8 +- .../loparkour/mode/impl/multi/CoopMode.java | 40 +++--- .../loparkour/mode/impl/multi/RaceMode.java | 8 +- .../mode/impl/multi/SpectatorMode.java | 29 ++--- .../loparkour/mode/impl/solo/DefaultMode.java | 11 +- .../mode/impl/solo/GravityShiftMode.java | 8 +- .../mode/impl/solo/SpeedrunMode.java | 25 ++-- .../loparkour/player/data/InventoryData.java | 22 ++-- .../loparkour/player/data/PreviousData.java | 3 +- .../player/service/UserRegistry.java | 6 +- .../player/service/ui/ScoreboardManager.java | 1 + .../player/spectator/ParkourSpectator.java | 26 ++-- .../loki/loparkour/reward/core/Reward.java | 31 ++--- .../loparkour/scheduler/BukkitBackend.java | 5 + .../loparkour/scheduler/FoliaBackend.java | 9 ++ .../loki/loparkour/scheduler/Scheduler.java | 14 ++ .../session/manager/SessionStateManager.java | 2 +- .../loparkour/storage/disk/StorageDisk.java | 7 +- .../storage/sql/SQLConnectionManager.java | 2 + .../loki/loparkour/util/item/Materials.java | 58 ++++----- .../dev/loki/loparkour/world/core/World.java | 12 +- src/main/resources/docs/SCHEMATICS.md | 26 ++-- src/main/resources/plugin.yml | 2 +- src/main/resources/schematics/schematics.yml | 4 +- .../339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a | 7 - .../f1e3c34b-9b02-40e6-83a6-0269e173eb83 | 3 - 79 files changed, 696 insertions(+), 800 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 388d77f..cae7d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ ### Новое -- **Ghost: личные реплеи забегов.** Серверный тумблер записи (`ghost-mode.enabled`) + персональный переключатель в меню настроек. Запоминаются последние 5 забегов игрока (`ghost-mode.max-runs`). Просмотр через `/lp replay [номер]`: вас телепортирует на старт записанного забега, и моделька игрока с вашим скином повторяет движение по блокам. -- **Coop-режим теперь работает.** `/lp join coop` создаёт общую сессию, друзья заходят через `/lp join <ник>`, общий счёт и статистика вклада. +- **Ghost: личные реплеи забегов.** Серверный тумблер записи (`ghost-mode.enabled`) + персональный переключатель в меню настроек. Запоминаются последние 5 забегов игрока (`ghost-mode.max-runs`). Просмотр через `/lpk replay [номер]`: вас телепортирует на старт записанного забега, и моделька игрока с вашим скином повторяет движение по блокам. +- **Coop-режим теперь работает.** `/lpk join coop` создаёт общую сессию, друзья заходят через `/lpk join <ник>`, общий счёт и статистика вклада. - **Мультиверсия в CI.** Сборка под Minecraft 1.19.2, 1.20.1, 1.20.4, 1.21, 1.21.11, 26.1.2, 26.2 (Java 21+). ### Исправления @@ -26,8 +26,8 @@ ### New -- **Ghost: personal run replays.** Server-wide recording toggle (`ghost-mode.enabled`) plus a per-player toggle in the settings menu. The last 5 runs per player are kept (`ghost-mode.max-runs`). Watch via `/lp replay [number]`: you are teleported to the start of the recorded run and a figure with your skin replays the movement block by block. -- **Coop mode now works.** `/lp join coop` creates a shared session, friends join via `/lp join `, shared score and contribution stats. +- **Ghost: personal run replays.** Server-wide recording toggle (`ghost-mode.enabled`) plus a per-player toggle in the settings menu. The last 5 runs per player are kept (`ghost-mode.max-runs`). Watch via `/lpk replay [number]`: you are teleported to the start of the recorded run and a figure with your skin replays the movement block by block. +- **Coop mode now works.** `/lpk join coop` creates a shared session, friends join via `/lpk join `, shared score and contribution stats. - **Multi-version CI.** Builds for Minecraft 1.19.2, 1.20.1, 1.20.4, 1.21, 1.21.11, 26.1.2, 26.2 (Java 21+). ### Fixes diff --git a/README.md b/README.md index 1536a43..a31d3de 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Advanced infinitely generating parkour plugin with ghost system, leaderboards an ![Java](https://img.shields.io/badge/Java-21+-orange?style=flat-square&logo=openjdk&logoColor=white) ![Paper](https://img.shields.io/badge/Paper-1.19.2+-blue?style=flat-square) -![Folia](https://img.shields.io/badge/Folia-supported-purple?style=flat-square) ![License](https://img.shields.io/badge/license-GPLv3-blue?style=flat-square&logo=gnu&logoColor=white) ![version](https://img.shields.io/badge/version-1.4.0-green?style=flat-square) @@ -53,14 +52,14 @@ LoParkour is an advanced parkour plugin with infinite world generation. The cour | Command | Description | |---------|-------------| -| `/lp join` | Join parkour | -| `/lp leave` | Leave parkour | -| `/lp spectate ` | Spectate another player | -| `/lp schematic save ` | Save a schematic | -| `/lp schematic reload` | Reload schematics | -| `/lp schematic convert` | Convert old vilib format | -| `/lp forcejoin ` | Force player to join (admin) | -| `/lp reset` | Reset leaderboards (admin) | +| `/lpk join` | Join parkour | +| `/lpk leave` | Leave parkour | +| `/lpk spectate ` | Spectate another player | +| `/lpk schematic save ` | Save a schematic | +| `/lpk schematic reload` | Reload schematics | +| `/lpk schematic convert` | Convert old vilib format | +| `/lpk forcejoin ` | Force player to join (admin) | +| `/lpk reset` | Reset leaderboards (admin) | ### Dependencies @@ -103,13 +102,13 @@ LoParkour - продвинутый паркур-плагин с бесконеч | Команда | Описание | |---------|----------| -| `/lp join` | Начать паркур | -| `/lp leave` | Выйти из паркура | -| `/lp spectate <игрок>` | Наблюдать за игроком | -| `/lp schematic save <имя>` | Сохранить схематику | -| `/lp schematic reload` | Перезагрузить схематики | -| `/lp forcejoin <игрок>` | Заставить игрока начать (админ) | -| `/lp reset` | Сбросить лидерборды (админ) | +| `/lpk join` | Начать паркур | +| `/lpk leave` | Выйти из паркура | +| `/lpk spectate <игрок>` | Наблюдать за игроком | +| `/lpk schematic save <имя>` | Сохранить схематику | +| `/lpk schematic reload` | Перезагрузить схематики | +| `/lpk forcejoin <игрок>` | Заставить игрока начать (админ) | +| `/lpk reset` | Сбросить лидерборды (админ) | ### Зависимости diff --git a/TODO.md b/TODO.md index 1504d03..71bef13 100644 --- a/TODO.md +++ b/TODO.md @@ -46,19 +46,32 @@ ### Функциональность, которая не работает -- **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. +- **21. Folia-поддержка — фикция.** ✅ **ЧАСТИЧНО (2026-10-10):** все вызовы планировщика идут через фасад `scheduler/` + (добавлен `runTimerAsync`: Bukkit → `runTaskTimerAsynchronously`, Folia → `AsyncScheduler.runAtFixedRate` рефлексией); + `MetricsCacheStore` переведён с `BukkitRunnable` на фасад, прямые `Bukkit.getScheduler()` из `DuelsCountdown`/`DuelsGenerator` + убраны — вне `scheduler/` их больше нет. Бейдж Folia из README убран (поддержка не проверена на Folia-сервере). + ❌ Осталось: тик идёт через global scheduler, но мутирует блоки/entities (`SessionStateManager`, `LifecycleTickManager`, + `BlockPlacer`, teleport) → нужен region/entity-tick + `folia-plugin.yml`; без Folia-стенда не верифицируется (см. п.143). - **22. Ghost-система — 100% мёртвый код.** `GhostManager`/`GhostRecorder`/`GhostPlayer` нигде не инстанцируются (grep: только само-ссылки). Конфиг `ghost-mode.*` не потребляется. → Подключить к lifecycle (запись в saveGhost/спавн в spawnGhosts) ИЛИ удалить подсистему и конфиг. - **23. Coop-режим нерабочий.** Ни одно `Session.create` не строит `CoopGenerator` (CoopMode.java:134). `/parkour join <игрок>` ведёт в спектаторы. → Реализовать создание coop-сессии (лобби/инвайты) или выпилить режим из меню и регистрации. - **24. Coop: латентный StackOverflow.** `ParkourPlayer.unregister()` → `CoopMode.leave()` → `ParkourUser.leave()` → `UserRegistry.unregister()` → `user.unregister()` — бесконечная рекурсия (ParkourPlayer.java:102-107, CoopMode.java:115-121, UserRegistry.java:58-67). Нет guard-флага. → Добавить in-flight guard в unregister (например, reentrancy flag в ParkourUser). - **25. Лидерборд: тайминги сломаны.** `Score.getTimeMillis()` ждёт ровно 3 части через `:` (Score.java:36-55), но генератор пишет `mm:ss.SSS` (2 части) и `HH:mm:ss.SSS` (третья `02.123` → NFE). Итог: TIME-сортировка и тай-брейк мёртвые, Race-лидерборд не обновляется. Плюс переполнение `int` в m*60*1000+... (Score.java:51). → Унифицировать формат времени; парсить `mm:ss.SSS` и `HH:mm:ss.SSS`; перейти на `long`. -- **26. ConfigUpdater уничтожает пользовательские данные.** `merge()` перегенерирует весь файл из шаблона при каждом старте (ConfigUpdater.java:85-127): теряются листы (`options.leads.amount`, стили), кастомные ключи, комментарии; алгоритм отступов (`prevIndent -= 2`) ошибочен; `ignoredSections` не защищают. → Точечный merge только отсутствующих ключей, сохранять листы/комментарии, либо заменить на библиотеку (e.g. `me.mattstudios` / SimpleConfig) или `Bukkit's config addDefaults`. -- **27. SQL: утечка Hikari-пулов.** `StorageSQL.init(mode)` вызывается на каждый режим (6 лидербордов), каждый раз пересоздаёт `connectionManager` (StorageSQL.java:66-77, LeaderboardStorage.java:35) → до ~10 соединений висят навсегда. → Один пул на плагин, инициализация один раз. -- **28. SQL: блокирующий connect на main thread** (SQLConnectionManager.java:34-56) — при лежащей БД включение висит до 5с×режимы. → `setInitializationFailTimeout(-1)` + асинхронный connect. -- **29. SQL: адаптив не стартует.** `AdaptiveServices.init` вызывается до создания лидербордов, а `StorageSQL.runWhenConnected` выбрасывает колбэк при не-подключении (StorageSQL.java:45-51). → Очередь колбэков в SQLConnectionManager.onConnectCallbacks; порядок init: лидерборды → storage → adaptive. +- **26. ConfigUpdater уничтожает пользовательские данные.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):** + точечный merge (`ConfigMergeProcessor` + `ConfigMergeParser`): block-sequence листы и `ignoredSections` сохраняются, + есть `ConfigMergeProcessorTest` (249 строк). `performUpdate` ловит только `IOException`, runtime уходит на границу `ConfigLoader`. +- **27. SQL: утечка Hikari-пулов.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):** + `StorageSQL.init(mode)` создаёт пул один раз (`if (connectionManager == null)`). +- **28. SQL: блокирующий connect на main thread** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):** + `setInitializationFailTimeout(-1)` — пул стартует, не дожидаясь БД. +- **29. SQL: адаптив не стартует.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):** + `SQLConnectionManager.runWhenConnected` складывает колбэки в `onConnectCallbacks` вместо выбрасывания. - **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) → курс зигзагом и не выпрямляется. → При диагонали аппендикс не перезаписывать случайным; корректный поворот-домножитель. +- **32. JumpCalculator: возврат невалидного прыжка.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):** + `validateAndRetryJump` + `safetyFallback` (3 ре-ролла шага 1/0, затем прямой шаг без джиттера, в крайнем случае `current`); + покрыто `JumpCalculatorTest` (`safetyFallbackReturnsValidStepForUnitHeading`, `...ReturnsCurrentWhenStraightStepImpossible`). +- **33. JumpCalculator: диагональный heading залипает.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):** + `offsetFor` считает перпендикулярный джиттер без перезаписи Z; покрыто `JumpCalculatorTest`. ### Генерация / lifecycle @@ -317,18 +330,17 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар - **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 на незачищенном остатке. +- **169. [M] Починить `ReturnCount` (69 warning / 52 файла) — ПЕРЕСТРУКТУРИРОВАНИЕМ, не порогом.** ✅ **ВЫПОЛНЕНО (2026-10-10):** все 69 void-методов переструктурированы (guard-`return` → `if`-блок или helper: `placePlayer`/`tickPlayer`/`boostIfValid`/`applyCommand`/`firstNonNull`), порог не поднимался, `NestedIfDepth` ≤ 2 соблюдён; правило переведено в `error`. Попутно `NPathComplexity` 12 → 9. Checkstyle: **0 errors / 61 warning**, тесты 96/0 fail. +- **170. [M] Починить `IllegalCatch` (23 warning / 18 файлов).** ✅ **ЧАСТИЧНО (2026-10-10, остаток 18 — осознанные границы, правило остаётся в warning):** + 3× `catch (Throwable)` → `catch (RuntimeException)` (диспетчер команд, session-tick, shutdown — `Error` больше не глотается); + сужено 5: `HoloHook` → `ClassNotFoundException | LinkageError`, `OptionGeneral` → `NumberFormatException` и + `IllegalArgumentException | IndexOutOfBoundsException`, `GeneratorProfileManager` → `IllegalArgumentException`, + `ConfigUpdater` → `IOException` (runtime уходит на границу `ConfigLoader`). + Не сужено осознанно (у каждого — комментарий «почему»): границы enable/reload/shutdown (`PluginBootstrap`, + `ConfigLoader`, `LocaleLoader`, `LoParkour:106`, `SQLConnectionManager:59` — таксономия ошибок HikariCP не публична), + порча данных (`InventoryData`, `PreviousData`, `SchematicManager` ×2), внешние API (`ScoreboardManager`/PAPI), + leave-путь (`UserRegistry`), изоляция колбэков (`SQLConnectionManager:106`), хот-путь генерации + (`JumpValidator`, `BlockSelector`), кэш сообщений (`LocaleCache`). После зачистки п.132–137 — удалить соответствующие записи из `config/checkstyle/suppressions.xml` (супрессии остаются только для ещё незачищенных legacy-файлов). @@ -600,7 +612,7 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар - **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. +- **144. [P1] Заменить команду `/lp` — конфликт с LuckPerms.** ✅ **ВЫПОЛНЕНО (2026-10-10, решение владельца: `lp`→`lpk`, `parkour` оставить).** `plugin.yml`: `aliases: [parkour, lpk]`; `/lp` → `/lpk` в `README.md`, `docs/SCHEMATICS.md`, `src/main/resources/docs/SCHEMATICS.md`, `schematics.yml`, `CHANGELOG.md`, javadoc `ReplayCommandExecutor`. Права (`loparkour.*`) не менялись. → Убрать `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-сообщение при вызове старого алиаса, если он останется. diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index fbc2357..9dc4363 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -68,7 +68,7 @@ - + diff --git a/docs/SCHEMATICS.md b/docs/SCHEMATICS.md index a4daaa8..4ec29fc 100644 --- a/docs/SCHEMATICS.md +++ b/docs/SCHEMATICS.md @@ -8,7 +8,7 @@ Jump structures are loaded from `plugins/LoParkour/schematics/` and matched to a |-----------|--------|--------| | `.nbt` | Vanilla structure block, `/structure save` | Pasted with Bukkit `StructureManager` | | `.schem`, `.schematic` | WorldEdit (`//copy`, `//schem save`) | Read via embedded WorldEdit clipboard IO | -| `.lpschem` | **Legacy LoParkour format** | Not used in-game; convert with `/lp schematic convert` | +| `.lpschem` | **Legacy LoParkour format** | Not used in-game; convert with `/lpk schematic convert` | During parkour generation, when the profile rolls **schematic** type, the plugin picks a random file whose `difficulty` in YAML is **≤** the player's schematic setting (0.0–1.0). @@ -26,23 +26,23 @@ Player setting **0.25 (easy)** → only structures with difficulty ≤ 0.25. ## Creating schematics in-game (admin) -1. Get the wand: `/lp schematic wand` -2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`) +1. Get the wand: `/lpk schematic wand` +2. **Left-click** = position 1, **right-click** = position 2 (or use `/lpk schematic pos1` / `pos2`) 3. Save and register: - - `/lp create ` — auto name `parkour-.schem` + YAML entry - - `/lp schematic create ` — same - - `/lp schematic create ` — custom name (letters, numbers, `-`, `_`) + - `/lpk create ` — auto name `parkour-.schem` + YAML entry + - `/lpk schematic create ` — same + - `/lpk schematic create ` — custom name (letters, numbers, `-`, `_`) Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1). -4. `/lp schematic reload` — reload files without restart +4. `/lpk schematic reload` — reload files without restart ## Converting old `.lpschem` files Place `.lpschem` files in `plugins/LoParkour/schematics/` or legacy `schematics-new/`, then: ``` -/lp schematic convert +/lpk schematic convert ``` Each file becomes `parkour-.schem` in `schematics/` and gets a `difficulty:` entry (from the old metadata, or `0.5` if missing). Original `.lpschem` files are kept; delete them manually after checking. @@ -51,14 +51,14 @@ Each file becomes `parkour-.schem` in `schematics/` and gets a `difficulty:` | Command | Description | |---------|-------------| -| `/lp schematic wand` | Selection wand | -| `/lp schematic pos1` / `pos2` | Set corners at your feet | -| `/lp create ` | Save selection → `.schem` + YAML | -| `/lp schematic create ` | Save with custom name | -| `/lp schematic convert` | Convert all `.lpschem` in schematics folders | -| `/lp schematic list` | List loaded structures | -| `/lp schematic paste ` | Test-paste at your location | -| `/lp schematic reload` | Reload disk files | +| `/lpk schematic wand` | Selection wand | +| `/lpk schematic pos1` / `pos2` | Set corners at your feet | +| `/lpk create ` | Save selection → `.schem` + YAML | +| `/lpk schematic create ` | Save with custom name | +| `/lpk schematic convert` | Convert all `.lpschem` in schematics folders | +| `/lpk schematic list` | List loaded structures | +| `/lpk schematic paste ` | Test-paste at your location | +| `/lpk schematic reload` | Reload disk files | ## Flow (runtime) diff --git a/src/main/java/dev/loki/loparkour/LoParkour.java b/src/main/java/dev/loki/loparkour/LoParkour.java index 52a44c7..c76a3a1 100644 --- a/src/main/java/dev/loki/loparkour/LoParkour.java +++ b/src/main/java/dev/loki/loparkour/LoParkour.java @@ -104,6 +104,7 @@ public final class LoParkour extends JavaPlugin { try { session.onAllPlayersLeft(); } catch (Exception e) { + // Boundary: one failing session must not abort server shutdown. getLogger().warning("Error cleaning up session: " + e.getMessage()); } }); @@ -125,7 +126,7 @@ public final class LoParkour extends JavaPlugin { } Storage.close(); World.delete(); - } catch (Throwable t) { + } catch (RuntimeException t) { getLogger().log(Level.SEVERE, "Error during plugin shutdown: " + t.getMessage(), t); } } diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/metrics/MetricsCacheStore.java b/src/main/java/dev/loki/loparkour/adaptive/core/metrics/MetricsCacheStore.java index 6da209f..f9cd08a 100644 --- a/src/main/java/dev/loki/loparkour/adaptive/core/metrics/MetricsCacheStore.java +++ b/src/main/java/dev/loki/loparkour/adaptive/core/metrics/MetricsCacheStore.java @@ -2,9 +2,10 @@ package dev.loki.loparkour.adaptive.core.metrics; import dev.loki.loparkour.adaptive.model.PlayerMetrics; import dev.loki.loparkour.adaptive.storage.StatsRepository; +import dev.loki.loparkour.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; -import org.bukkit.scheduler.BukkitRunnable; import java.util.Map; import java.util.UUID; @@ -29,7 +30,7 @@ public class MetricsCacheStore { private final StatsRepository repository; private final Map metricsCache; - private BukkitRunnable autoSaveTask; + private ScheduledTask autoSaveTask; public MetricsCacheStore(@NotNull Plugin plugin, @NotNull StatsRepository repository) { this.plugin = plugin; @@ -81,14 +82,8 @@ public class MetricsCacheStore { autoSaveTask.cancel(); } - autoSaveTask = new BukkitRunnable() { - @Override - public void run() { - saveAll(); - } - }; - - autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS); + autoSaveTask = Scheduler.get(plugin).runTimerAsync(this::saveAll, + AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS); } public void stopAutoSave() { diff --git a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java index 7cc9b25..90ecada 100644 --- a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java +++ b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java @@ -123,6 +123,7 @@ public final class PluginBootstrap { World.create(); plugin.getLogger().info("Parkour world created successfully!"); } catch (Exception ex) { + // Boundary: world creation must never break enable; provider behavior varies across versions. plugin.getLogger().log(Level.SEVERE, "Failed to create parkour world", ex); } } diff --git a/src/main/java/dev/loki/loparkour/command/admin/ops/ForceJoinExecutor.java b/src/main/java/dev/loki/loparkour/command/admin/ops/ForceJoinExecutor.java index 76b6ac0..afed0b1 100644 --- a/src/main/java/dev/loki/loparkour/command/admin/ops/ForceJoinExecutor.java +++ b/src/main/java/dev/loki/loparkour/command/admin/ops/ForceJoinExecutor.java @@ -26,13 +26,11 @@ public final class ForceJoinExecutor { } if ("everyone".equalsIgnoreCase(target)) { joinEveryone(sender); - return; - } - if ("nearest".equalsIgnoreCase(target)) { + } else if ("nearest".equalsIgnoreCase(target)) { joinNearest(sender); - return; + } else { + joinByName(target, sender); } - joinByName(target, sender); } private void joinEveryone(CommandSender sender) { diff --git a/src/main/java/dev/loki/loparkour/command/admin/ops/ForceLeaveExecutor.java b/src/main/java/dev/loki/loparkour/command/admin/ops/ForceLeaveExecutor.java index d6a510e..a5539ac 100644 --- a/src/main/java/dev/loki/loparkour/command/admin/ops/ForceLeaveExecutor.java +++ b/src/main/java/dev/loki/loparkour/command/admin/ops/ForceLeaveExecutor.java @@ -26,9 +26,9 @@ public final class ForceLeaveExecutor { } if ("everyone".equalsIgnoreCase(target)) { leaveEveryone(sender); - return; + } else { + leaveByName(target, sender); } - leaveByName(target, sender); } private void leaveEveryone(CommandSender sender) { @@ -38,15 +38,13 @@ public final class ForceLeaveExecutor { private void leaveByName(String target, CommandSender sender) { final Player other = Bukkit.getPlayer(target); + final ParkourUser user = other == null ? null : ParkourUser.getUser(other); if (other == null) { AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); - return; - } - final ParkourUser user = ParkourUser.getUser(other); - if (user == null) { + } else if (user == null) { AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing")); - return; + } else { + ParkourUser.leave(user); } - ParkourUser.leave(user); } } diff --git a/src/main/java/dev/loki/loparkour/command/admin/ops/RecoverInventoryExecutor.java b/src/main/java/dev/loki/loparkour/command/admin/ops/RecoverInventoryExecutor.java index ae31345..752d2d4 100644 --- a/src/main/java/dev/loki/loparkour/command/admin/ops/RecoverInventoryExecutor.java +++ b/src/main/java/dev/loki/loparkour/command/admin/ops/RecoverInventoryExecutor.java @@ -26,9 +26,9 @@ public final class RecoverInventoryExecutor { final Player other = Bukkit.getPlayer(target); if (other == null) { AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); - return; + } else { + recover(other, sender); } - recover(other, sender); } private void recover(Player other, CommandSender sender) { diff --git a/src/main/java/dev/loki/loparkour/command/admin/ops/ResetExecutor.java b/src/main/java/dev/loki/loparkour/command/admin/ops/ResetExecutor.java index bed3760..c271223 100644 --- a/src/main/java/dev/loki/loparkour/command/admin/ops/ResetExecutor.java +++ b/src/main/java/dev/loki/loparkour/command/admin/ops/ResetExecutor.java @@ -29,9 +29,9 @@ public final class ResetExecutor { } if ("everyone".equalsIgnoreCase(target)) { resetEveryone(sender); - return; + } else { + resetPlayer(target, sender); } - resetPlayer(target, sender); } private void resetEveryone(CommandSender sender) { diff --git a/src/main/java/dev/loki/loparkour/command/core/CommandRouter.java b/src/main/java/dev/loki/loparkour/command/core/CommandRouter.java index 4cb57ae..232f193 100644 --- a/src/main/java/dev/loki/loparkour/command/core/CommandRouter.java +++ b/src/main/java/dev/loki/loparkour/command/core/CommandRouter.java @@ -54,16 +54,13 @@ class CommandRouter { private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) { if (isAdminCommand(a1)) { admin.handle(a1, a2, sender, p); - return; + } else if (p != null) { + if (canCreateSchematic(a1, p)) { + SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player); + } else { + handlePlayerSubcommand(a1, a2, sender, p); + } } - if (p == null) { - return; - } - if (canCreateSchematic(a1, p)) { - SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player); - return; - } - handlePlayerSubcommand(a1, a2, sender, p); } private static boolean isAdminCommand(String a1) { @@ -95,32 +92,24 @@ class CommandRouter { } private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) { - if (p == null) { + if (p == null || !p.hasPermission(ParkourOption.ADMIN.permission)) { return; } - if (!p.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - if ("schematic".equalsIgnoreCase(a1)) { if ("create".equalsIgnoreCase(a2)) { SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player); - return; + } else { + SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p); } - SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p); } } private void handle4(String a1, String a2, String a3, String a4, CommandSender sender, @Nullable Player p) { - if (p == null) { + if (p == null || !p.hasPermission(ParkourOption.ADMIN.permission)) { return; } - if (!p.hasPermission(ParkourOption.ADMIN.permission)) { - return; + if ("schematic".equalsIgnoreCase(a1) && "create".equalsIgnoreCase(a2)) { + SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player); } - if (!"schematic".equalsIgnoreCase(a1) || !"create".equalsIgnoreCase(a2)) { - return; - } - SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player); } } diff --git a/src/main/java/dev/loki/loparkour/command/core/LoParkourCommand.java b/src/main/java/dev/loki/loparkour/command/core/LoParkourCommand.java index 2d305dc..eeeda1b 100644 --- a/src/main/java/dev/loki/loparkour/command/core/LoParkourCommand.java +++ b/src/main/java/dev/loki/loparkour/command/core/LoParkourCommand.java @@ -33,7 +33,7 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { final Player p = sender instanceof Player ? (Player) sender : null; router.dispatch(args, sender, p); return true; - } catch (Throwable t) { + } catch (RuntimeException t) { LoParkour.getPlugin().getLogger().log(Level.SEVERE, "LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t); Locales.send(sender, "commands.error"); diff --git a/src/main/java/dev/loki/loparkour/command/player/JoinCommandExecutor.java b/src/main/java/dev/loki/loparkour/command/player/JoinCommandExecutor.java index f12c8af..2745e69 100644 --- a/src/main/java/dev/loki/loparkour/command/player/JoinCommandExecutor.java +++ b/src/main/java/dev/loki/loparkour/command/player/JoinCommandExecutor.java @@ -27,23 +27,21 @@ public class JoinCommandExecutor { final Mode mode = Registry.getMode(arg); if (mode != null) { mode.create(p); - return; + } else { + joinPlayingPlayer(arg, sender, p); } - joinPlayingPlayer(arg, sender, p); } private void joinPlayingPlayer(String arg, CommandSender sender, Player p) { final Player other = Bukkit.getPlayer(arg); + final ParkourPlayer pp = other == null ? null : UserRegistry.getPlayer(other); if (other == null) { CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player")); - return; - } - final ParkourPlayer pp = UserRegistry.getPlayer(other); - if (pp == null) { + } else if (pp == null) { CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing")); - return; + } else { + joinSession(sender, p, pp.session); } - joinSession(sender, p, pp.session); } private void joinSession(CommandSender sender, Player p, Session session) { diff --git a/src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java b/src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java index b8902da..367e708 100644 --- a/src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java +++ b/src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java @@ -55,10 +55,9 @@ public class PlayerCommandHandler { switch (lower) { case "join" -> handleJoin(sender, player); case "leave" -> { - if (!cooldown(sender, "leave", COOLDOWN_MS)) { - return; + if (cooldown(sender, "leave", COOLDOWN_MS)) { + ParkourUser.leave(player); } - ParkourUser.leave(player); } case "play" -> { if (ParkourOption.PLAY.mayPerform(player)) { @@ -71,19 +70,19 @@ public class PlayerCommandHandler { } } case "leaderboard" -> { - if (!ParkourOption.LEADERBOARDS.mayPerform(player)) { + if (ParkourOption.LEADERBOARDS.mayPerform(player)) { + Menus.LEADERBOARDS.open(player); + } else { send(sender, Locales.getString(player, "other.no_do")); - return; } - Menus.LEADERBOARDS.open(player); } case "replay" -> new ReplayCommandExecutor().replay(null, sender, player, this); case "schematic" -> { - if (!player.hasPermission(ParkourOption.ADMIN.permission)) { + if (player.hasPermission(ParkourOption.ADMIN.permission)) { + SchematicCommandHandler.sendHelp(player); + } else { send(sender, Locales.getString(player, "other.no_do")); - return; } - SchematicCommandHandler.sendHelp(player); } default -> { } @@ -96,12 +95,9 @@ public class PlayerCommandHandler { } if (!ParkourOption.JOIN.mayPerform(player)) { send(sender, Locales.getString(player, "other.no_do")); - return; + } else if (ParkourUser.getUser(player) == null) { + Modes.defaultMode().create(player); } - if (ParkourUser.getUser(player) != null) { - return; - } - Modes.defaultMode().create(player); } private void handleReload(@NotNull CommandSender sender, @Nullable Player player) { @@ -110,11 +106,11 @@ public class PlayerCommandHandler { } if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { send(sender, Locales.getString(sender, "other.no_do")); - return; + } else { + Config.reload(false); + dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.reload(); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.reload")); } - Config.reload(false); - dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.reload(); - send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.reload")); } public void sendHelp(@NotNull CommandSender sender) { diff --git a/src/main/java/dev/loki/loparkour/command/player/ReplayCommandExecutor.java b/src/main/java/dev/loki/loparkour/command/player/ReplayCommandExecutor.java index e2b7af9..6a99665 100644 --- a/src/main/java/dev/loki/loparkour/command/player/ReplayCommandExecutor.java +++ b/src/main/java/dev/loki/loparkour/command/player/ReplayCommandExecutor.java @@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable; import java.util.List; /** - * Handles {@code /lp replay [index]}: plays back a player's recorded ghost runs. + * Handles {@code /lpk replay [index]}: plays back a player's recorded ghost runs. */ public class ReplayCommandExecutor { @@ -30,14 +30,18 @@ public class ReplayCommandExecutor { final GhostService ghostService = LoParkour.getGhostService(); if (ghostService == null || !Config.CONFIG.getBoolean("ghost-mode.enabled")) { CommandUtil.send(sender, Locales.getString(sender, "commands.replay.disabled")); - return; + } else { + startRun(indexArg, sender, p, ghostService); } + } + + private static void startRun(@Nullable String indexArg, @NotNull CommandSender sender, @NotNull Player p, + @NotNull GhostService ghostService) { final GhostData run = resolveRun(indexArg, sender, p, ghostService); - if (run == null) { - return; + if (run != null) { + ghostService.startReplay(p, run); + CommandUtil.send(sender, Locales.getString(sender, "commands.replay.starting")); } - ghostService.startReplay(p, run); - CommandUtil.send(sender, Locales.getString(sender, "commands.replay.starting")); } private static GhostData resolveRun(@Nullable String indexArg, @NotNull CommandSender sender, diff --git a/src/main/java/dev/loki/loparkour/command/schematic/SchematicHandler.java b/src/main/java/dev/loki/loparkour/command/schematic/SchematicHandler.java index c557617..481a208 100644 --- a/src/main/java/dev/loki/loparkour/command/schematic/SchematicHandler.java +++ b/src/main/java/dev/loki/loparkour/command/schematic/SchematicHandler.java @@ -35,7 +35,15 @@ final class SchematicHandler { SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete")); return; } + createSelected(sel, optionalName, difficultyRaw, player); + } + private static void createSelected( + Location[] sel, + @Nullable String optionalName, + @NotNull String difficultyRaw, + Player player + ) { final double difficulty; try { difficulty = SchematicCreator.parseDifficulty(difficultyRaw); @@ -60,18 +68,17 @@ final class SchematicHandler { static void paste(String name, CommandSender sender, Player player) { if (!isValidSchematicName(name)) { SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name")); - return; + } else { + final ParkourSchematic schematic = LoParkour.getSchematicManager().get(name); + if (schematic == null) { + SchematicMessenger.send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found") + .formatted(name)); + } else { + schematic.paste(player.getLocation(), player.getWorld()); + SchematicMessenger.send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted") + .formatted(name)); + } } - - final ParkourSchematic schematic = LoParkour.getSchematicManager().get(name); - if (schematic == null) { - SchematicMessenger.send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found") - .formatted(name)); - return; - } - schematic.paste(player.getLocation(), player.getWorld()); - SchematicMessenger.send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted") - .formatted(name)); } static void posCommand(Player player, Location[] existing, Location loc, int index) { diff --git a/src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java b/src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java index b47639d..d403c7f 100644 --- a/src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java +++ b/src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java @@ -42,6 +42,7 @@ public final class ConfigLoader { ConfigUpdater.update(configFile, resourceStream, "1.0", ignoredSections, plugin.getLogger()); } } catch (Exception ex) { + // Boundary: config sync must never break enable/reload; template or disk may be corrupt. plugin.getLogger().log(java.util.logging.Level.SEVERE, "Error while trying to update config file: " + fileName, ex); } diff --git a/src/main/java/dev/loki/loparkour/config/core/merge/ConfigUpdater.java b/src/main/java/dev/loki/loparkour/config/core/merge/ConfigUpdater.java index 696f3d3..02d75ce 100644 --- a/src/main/java/dev/loki/loparkour/config/core/merge/ConfigUpdater.java +++ b/src/main/java/dev/loki/loparkour/config/core/merge/ConfigUpdater.java @@ -46,7 +46,7 @@ public final class ConfigUpdater { final List merged = new ConfigMergeProcessor().merge(templateStream, configFile, ignoredKeys); Files.write(configFile.toPath(), merged); logger.info("Configuration file synced: " + configFile.getName()); - } catch (Exception e) { + } catch (IOException e) { if (backup != null && backup.exists()) { Files.copy(backup.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING); logger.severe("Config sync failed, restored backup: " + e.getMessage()); diff --git a/src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java b/src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java index ced235f..3347a02 100644 --- a/src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java +++ b/src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java @@ -42,6 +42,7 @@ public class LocaleCache { final T result = extractor.apply(config); return result != null ? result : defaultValue; } catch (Exception e) { + // Boundary: message formatting must never throw; locale data is arbitrary. return defaultValue; } } diff --git a/src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java b/src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java index 62855fb..1bfa90b 100644 --- a/src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java +++ b/src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java @@ -41,6 +41,7 @@ public final class LocaleLoader { return loadFromDisk(embedded); } catch (Exception ex) { + // Boundary: locale loading must never break enable; disk files may be corrupt. plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage()); return Collections.emptyMap(); } diff --git a/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java b/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java index 57659fa..893f317 100644 --- a/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java +++ b/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java @@ -139,7 +139,7 @@ public final class OptionGeneral { goBackLocation.setYaw(Float.parseFloat(axes[1].trim())); } } - } catch (Exception ex) { + } catch (NumberFormatException ex) { LoParkour.getPlugin().getLogger().warning("Invalid bungeecord.go-back-axes, using 0,0"); } } @@ -181,7 +181,7 @@ public final class OptionGeneral { } return new Location(world, Double.parseDouble(values[0]), Double.parseDouble(values[1]), Double.parseDouble(values[2])); - } catch (Exception ex) { + } catch (IllegalArgumentException | IndexOutOfBoundsException ex) { LoParkour.getPlugin().getLogger().warning( "Invalid bungeecord.go-back value '" + location + "'. Falling back to spawn world origin."); final World fallback = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0); diff --git a/src/main/java/dev/loki/loparkour/duels/DuelsCountdown.java b/src/main/java/dev/loki/loparkour/duels/DuelsCountdown.java index be1c3a8..e2a4cf3 100644 --- a/src/main/java/dev/loki/loparkour/duels/DuelsCountdown.java +++ b/src/main/java/dev/loki/loparkour/duels/DuelsCountdown.java @@ -3,10 +3,10 @@ package dev.loki.loparkour.duels; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.config.locale.Locales; import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; import dev.loki.loparkour.util.text.ColorUtil; -import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; @@ -35,14 +35,14 @@ final class DuelsCountdown { private static final String DEFAULT_COLOR = "E120"; private final DuelsGenerator generator; - private BukkitTask countdownTask; + private ScheduledTask countdownTask; DuelsCountdown(@NotNull DuelsGenerator generator) { this.generator = generator; } void giveStartItem(@NotNull ParkourPlayer owner) { - Bukkit.getScheduler().runTaskLater(LoParkour.getPlugin(), () -> { + Scheduler.get(LoParkour.getPlugin()).runLater(() -> { if (owner.player.isOnline()) { owner.player.getInventory().addItem(Locales.getItem(owner.locale(), "play.multi.duels.start").build()); } @@ -52,7 +52,7 @@ final class DuelsCountdown { void start() { cancel(); final int[] counter = {COUNTDOWN_SECONDS}; - countdownTask = Bukkit.getScheduler().runTaskTimer(LoParkour.getPlugin(), + countdownTask = Scheduler.get(LoParkour.getPlugin()).runTimer( () -> tick(counter), 0L, COUNTDOWN_PERIOD); } @@ -66,15 +66,13 @@ final class DuelsCountdown { void tick(@NotNull int[] counter) { if (generator.state.stopped()) { cancel(); - return; - } - final int current = counter[0]--; - if (current == 0) { - startRace(); - return; - } - if (current <= COUNTDOWN_COLOR_START) { - sendCountdownTitle(current); + } else { + final int current = counter[0]--; + if (current == 0) { + startRace(); + } else if (current <= COUNTDOWN_COLOR_START) { + sendCountdownTitle(current); + } } } diff --git a/src/main/java/dev/loki/loparkour/duels/DuelsGenerator.java b/src/main/java/dev/loki/loparkour/duels/DuelsGenerator.java index 01de3a8..7d3fa2c 100644 --- a/src/main/java/dev/loki/loparkour/duels/DuelsGenerator.java +++ b/src/main/java/dev/loki/loparkour/duels/DuelsGenerator.java @@ -8,12 +8,13 @@ import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; import dev.loki.loparkour.mode.base.Mode; import dev.loki.loparkour.mode.base.Modes; import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.scheduler.Scheduler; import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.util.text.ColorUtil; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -87,46 +88,45 @@ public class DuelsGenerator extends ParkourGenerator { } public void addPlayer(@NotNull ParkourPlayer pp) { - if (!allowJoining || playerGenerators.containsKey(pp)) { - return; + if (allowJoining && !playerGenerators.containsKey(pp)) { + final SingleDuelsGenerator gen = new SingleDuelsGenerator(session); + gen.setOwningGenerator(this); + gen.setOwnedPlayer(pp); + gen.setPlayerIndex(playerGenerators.size()); + gen.state.setHeading(OptionGeneral.heading().getDirection()); + placePlayer(pp, gen, session.getSpawnLocation()); } - final SingleDuelsGenerator gen = new SingleDuelsGenerator(session); - gen.setOwningGenerator(this); - gen.setOwnedPlayer(pp); - gen.setPlayerIndex(playerGenerators.size()); - gen.state.setHeading(OptionGeneral.heading().getDirection()); - final Location spawnBase = session.getSpawnLocation(); - if (spawnBase == null || spawnBase.getWorld() == null) { - return; - } - Location platformCenter = spawnBase; - if (pp != owner) { - platformCenter = spawnBase.clone().add(0, 0, islandDistance * (double) playerGenerators.size()); - for (int dx = -PLATFORM_RADIUS; dx <= PLATFORM_RADIUS; dx++) { - for (int dz = -PLATFORM_RADIUS; dz <= PLATFORM_RADIUS; dz++) { - platformCenter.getBlock().getRelative(dx, 0, dz).setType(Material.STONE); + } + + private void placePlayer(@NotNull ParkourPlayer pp, @NotNull SingleDuelsGenerator gen, + @Nullable Location spawnBase) { + if (spawnBase != null && spawnBase.getWorld() != null) { + Location platformCenter = spawnBase; + if (pp != owner) { + platformCenter = spawnBase.clone().add(0, 0, islandDistance * (double) playerGenerators.size()); + for (int dx = -PLATFORM_RADIUS; dx <= PLATFORM_RADIUS; dx++) { + for (int dz = -PLATFORM_RADIUS; dz <= PLATFORM_RADIUS; dz++) { + platformCenter.getBlock().getRelative(dx, 0, dz).setType(Material.STONE); + } } } + final Location playerSpawn = platformCenter.clone().add(SPAWN_CENTER_OFFSET, 1, SPAWN_CENTER_OFFSET); + playerSpawns.put(pp, playerSpawn); + playerGenerators.put(pp, gen); + pp.player.teleport(playerSpawn); } - final Location playerSpawn = platformCenter.clone().add(SPAWN_CENTER_OFFSET, 1, SPAWN_CENTER_OFFSET); - playerSpawns.put(pp, playerSpawn); - playerGenerators.put(pp, gen); - pp.player.teleport(playerSpawn); } public void removePlayer(@NotNull ParkourPlayer pp) { final SingleDuelsGenerator gen = playerGenerators.remove(pp); - if (gen == null) { - return; - } - gen.reset(false); - if (pp == owner) { - countdown.cancel(); - returnToLobby(); - return; - } - if (playerGenerators.size() == 1) { - win(new ArrayList<>(playerGenerators.keySet()).get(0)); + if (gen != null) { + gen.reset(false); + if (pp == owner) { + countdown.cancel(); + returnToLobby(); + } else if (playerGenerators.size() == 1) { + win(new ArrayList<>(playerGenerators.keySet()).get(0)); + } } } @@ -150,7 +150,7 @@ public class DuelsGenerator extends ParkourGenerator { pp.player.sendTitle(ColorUtil.color(Locales.getString(pp.locale(), pp == winner ? "play.multi.duels.victory" : "play.multi.duels.loss")), "", VICTORY_FADE_IN, VICTORY_STAY, VICTORY_FADE_OUT); } - Bukkit.getScheduler().runTaskLater(LoParkour.getPlugin(), () -> returnToLobby(), WIN_RETURN_DELAY_TICKS); + Scheduler.get(LoParkour.getPlugin()).runLater(this::returnToLobby, WIN_RETURN_DELAY_TICKS); } @Override diff --git a/src/main/java/dev/loki/loparkour/elytra/close/CloseMode.java b/src/main/java/dev/loki/loparkour/elytra/close/CloseMode.java index 67ed77a..6960a19 100644 --- a/src/main/java/dev/loki/loparkour/elytra/close/CloseMode.java +++ b/src/main/java/dev/loki/loparkour/elytra/close/CloseMode.java @@ -44,12 +44,9 @@ public class CloseMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof CloseMode) { - return; + if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof CloseMode)) { + player.closeInventory(); + Session.create(CloseGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(CloseGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/elytra/core/ElytraBoost.java b/src/main/java/dev/loki/loparkour/elytra/core/ElytraBoost.java index 53c2fde..ba4dbe9 100644 --- a/src/main/java/dev/loki/loparkour/elytra/core/ElytraBoost.java +++ b/src/main/java/dev/loki/loparkour/elytra/core/ElytraBoost.java @@ -28,19 +28,21 @@ final class ElytraBoost { private int boostedSectionIdx = Integer.MIN_VALUE; void tick(Player player, ElytraSection section, int idx) { - if (!section.isAscending()) { - return; + if (section.isAscending()) { + boostIfValid(player, section, idx); } + } + + private void boostIfValid(Player player, ElytraSection section, int idx) { final Vector pos = player.getLocation().toVector(); final Vector direction = section.end().clone().subtract(section.beginning()).normalize(); - if (direction.lengthSquared() < EPSILON) { - return; - } - if (idx != boostedSectionIdx && section.isNearKnot(pos, 0, NEAR_KNOT_DISTANCE)) { - applyInitialBoost(player, idx, direction); - } - if (pos.getX() >= section.beginning().getX() && pos.getX() < section.end().getX()) { - sustain(player, direction); + if (direction.lengthSquared() >= EPSILON) { + if (idx != boostedSectionIdx && section.isNearKnot(pos, 0, NEAR_KNOT_DISTANCE)) { + applyInitialBoost(player, idx, direction); + } + if (pos.getX() >= section.beginning().getX() && pos.getX() < section.end().getX()) { + sustain(player, direction); + } } } diff --git a/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java b/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java index be218f9..4dc6796 100644 --- a/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java @@ -15,6 +15,7 @@ import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Random; @@ -93,25 +94,27 @@ public class ElytraGenerator extends ParkourGenerator { @Override public void tick() { final Player player = getPlayer(); - if (player == null || sections.isEmpty()) { - return; + if (player != null && !sections.isEmpty()) { + tickPlayer(player); } + } + + private void tickPlayer(@NotNull Player player) { final int idx = sections.keySet().stream().max(Integer::compareTo).orElse(-1); final ElytraSection section = sections.get(idx); - if (section == null) { - return; - } - final Vector pos = player.getLocation().toVector(); - applyScoring(player, pos); - blockChanger.check(player, material); - removeOldest(player); - if (shouldReset(player, pos)) { - reset(true); - return; - } - boost.tick(player, section, idx); - if (section.isNearKnot(pos, 2, GENERATE_DISTANCE)) { - generate(); + if (section != null) { + final Vector pos = player.getLocation().toVector(); + applyScoring(player, pos); + blockChanger.check(player, material); + removeOldest(player); + if (shouldReset(player, pos)) { + reset(true); + } else { + boost.tick(player, section, idx); + if (section.isNearKnot(pos, 2, GENERATE_DISTANCE)) { + generate(); + } + } } } diff --git a/src/main/java/dev/loki/loparkour/elytra/core/ElytraMode.java b/src/main/java/dev/loki/loparkour/elytra/core/ElytraMode.java index 59c3785..ff5bce4 100644 --- a/src/main/java/dev/loki/loparkour/elytra/core/ElytraMode.java +++ b/src/main/java/dev/loki/loparkour/elytra/core/ElytraMode.java @@ -44,12 +44,9 @@ public class ElytraMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ElytraMode) { - return; + if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ElytraMode)) { + player.closeInventory(); + Session.create(ElytraGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(ElytraGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedMode.java b/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedMode.java index ef6f25d..b17afb9 100644 --- a/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedMode.java +++ b/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedMode.java @@ -44,12 +44,9 @@ public class MinSpeedMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof MinSpeedMode) { - return; + if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof MinSpeedMode)) { + player.closeInventory(); + Session.create(MinSpeedGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(MinSpeedGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java b/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java index edc3913..ddc2bbe 100644 --- a/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java @@ -46,15 +46,11 @@ public class ObstacleGenerator extends ElytraGenerator { private void generateObstacle(int idx, ElytraSection section, int knotIdx) { final Obstacle obstacle = Obstacle.values()[ThreadLocalRandom.current().nextInt(Obstacle.values().length)]; final List points = obstacle.getPoints(section.getKnot(knotIdx), radius()); - if (points.isEmpty()) { - return; - } final Player player = getPlayer(); - if (player == null) { - return; + if (!points.isEmpty() && player != null) { + blockChanger.sendNow(player, points, Material.YELLOW_CONCRETE); + obstacles.computeIfAbsent(idx, key -> new ArrayList<>()).addAll(points); } - blockChanger.sendNow(player, points, Material.YELLOW_CONCRETE); - obstacles.computeIfAbsent(idx, key -> new ArrayList<>()).addAll(points); } @Override diff --git a/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleMode.java b/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleMode.java index 06a464d..ce4210d 100644 --- a/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleMode.java +++ b/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleMode.java @@ -44,12 +44,9 @@ public class ObstacleMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ObstacleMode) { - return; + if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ObstacleMode)) { + player.closeInventory(); + Session.create(ObstacleGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(ObstacleGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonMode.java b/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonMode.java index b96d9ba..7ca134b 100644 --- a/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonMode.java +++ b/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonMode.java @@ -44,12 +44,9 @@ public class SpeedDemonMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof SpeedDemonMode) { - return; + if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof SpeedDemonMode)) { + player.closeInventory(); + Session.create(SpeedDemonGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(SpeedDemonGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialMode.java b/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialMode.java index ad4aacf..ed8e745 100644 --- a/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialMode.java +++ b/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialMode.java @@ -44,12 +44,9 @@ public class TimeTrialMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof TimeTrialMode) { - return; + if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof TimeTrialMode)) { + player.closeInventory(); + Session.create(TimeTrialGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(TimeTrialGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java index ad63d5c..5659a64 100644 --- a/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java +++ b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java @@ -170,8 +170,8 @@ public class GeneratorProfileManager { } normalizeMap(generator.state.specialChances); - } catch (Exception ignored) { - // special blocks config optional + } catch (IllegalArgumentException ignored) { + // special blocks config optional: bad numbers/materials only disable specials } } diff --git a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java index 0ff636e..847810b 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java @@ -67,7 +67,7 @@ public class JumpValidator { } } } catch (Exception ignored) { - // Mock blocks in tests cannot be inspected; fall back to no adjustment + // Boundary: block inspection may fail on mocks or across versions; fall back to no adjustment. } return 0.0; // No adjustment for full blocks } @@ -82,44 +82,31 @@ public class JumpValidator { final double dy = to.getY() - from.getY(); final double dz = to.getZ() - from.getZ(); - // Check if same location (no jump) + // Same-location check (no jump) final double horizontalDistance = Math.sqrt(dx * dx + dz * dz); - if (horizontalDistance < SAME_LOCATION_EPSILON && Math.abs(dy) < SAME_LOCATION_EPSILON) { - return false; // Same location - } - - // Check horizontal distance limit - if (horizontalDistance > maxHorizontal) { - return false; - } + final boolean sameLocation = horizontalDistance < SAME_LOCATION_EPSILON && Math.abs(dy) < SAME_LOCATION_EPSILON; // Adjust vertical limits based on slab height - double adjustedMaxVerticalUp = maxVerticalUp; - double adjustedMaxVerticalDown = maxVerticalDown; + final double adjustedMaxVerticalUp = heightAdjustment < 0 + ? maxVerticalUp + heightAdjustment + : maxVerticalUp; + final double adjustedMaxVerticalDown = heightAdjustment < 0 + ? maxVerticalDown - heightAdjustment + : maxVerticalDown; - if (heightAdjustment < 0) { - // Player is lower (bottom slab), reduce upward jump capability - adjustedMaxVerticalUp = maxVerticalUp + heightAdjustment; // Reduce max up jump - adjustedMaxVerticalDown = maxVerticalDown - heightAdjustment; // Increase max down jump - } + // For upward jumps, reduce max horizontal distance (player can't jump as far when jumping up) + final double effectiveMaxHorizontal = dy > 0 + ? maxHorizontal - (dy * VERTICAL_REDUCTION_FACTOR) + : maxHorizontal; - // Check vertical limits with adjustment - if (dy > adjustedMaxVerticalUp || dy < -adjustedMaxVerticalDown) { - return false; - } - - // For upward jumps, reduce max horizontal distance - // Player can't jump as far when jumping up - if (dy > 0) { - final double adjustedMaxHorizontal = maxHorizontal - (dy * VERTICAL_REDUCTION_FACTOR); // Reduce by 0.5 blocks per block up - if (horizontalDistance > adjustedMaxHorizontal) { - return false; - } - } - - // Check total distance + final boolean verticalOk = dy <= adjustedMaxVerticalUp && dy >= -adjustedMaxVerticalDown; final double totalDistance = Math.sqrt(dx * dx + dy * dy + dz * dz); - return totalDistance <= maxDistance; + + return !sameLocation + && horizontalDistance <= maxHorizontal + && verticalOk + && horizontalDistance <= effectiveMaxHorizontal + && totalDistance <= maxDistance; } // Check if jump is possible: sqrt(dx² + dy² + dz²) <= maxDistance diff --git a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java index 1977357..b1b92e2 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java @@ -95,32 +95,27 @@ public class BlockPlacer { final boolean tall = targetSelector.isTallMaterial(blockData.getMaterial()); final List blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks(); - if (blocks.isEmpty()) { - return; + if (!blocks.isEmpty()) { + final Block selectedBlock = blocks.get(0); + worldPlacer.place(selectedBlock, blockData); + generator.state.history.add(selectedBlock); + generator.state.addGeneratedBlocks(1); } - - final Block selectedBlock = blocks.get(0); - worldPlacer.place(selectedBlock, blockData); - generator.state.history.add(selectedBlock); - generator.state.addGeneratedBlocks(1); } private void placeSpecialBlock() { final BlockData specialBlockData = Probs.random(generator.state.specialChances); if (specialBlockData == null) { placeNormalBlock(); - return; + } else { + final boolean tall = targetSelector.isTallMaterial(specialBlockData.getMaterial()); + final List blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks(); + if (!blocks.isEmpty()) { + final Block selectedBlock = blocks.get(0); + worldPlacer.place(selectedBlock, specialBlockData); + generator.state.history.add(selectedBlock); + generator.state.addGeneratedBlocks(1); + } } - - final boolean tall = targetSelector.isTallMaterial(specialBlockData.getMaterial()); - final List blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks(); - if (blocks.isEmpty()) { - return; - } - - final Block selectedBlock = blocks.get(0); - worldPlacer.place(selectedBlock, specialBlockData); - generator.state.history.add(selectedBlock); - generator.state.addGeneratedBlocks(1); } } diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java index 18e480b..6a4bc02 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java @@ -88,18 +88,15 @@ public class LifecycleEventManager { } private void applyOneTimeRewards(@NotNull ParkourPlayer player, @NotNull Mode mode, int score) { - if (!Rewards.oneTimeRewards().containsKey(score)) { - return; - } final String rewardKey = String.valueOf(score); - if (player.settings().collectedRewards().contains(rewardKey)) { - return; + if (Rewards.oneTimeRewards().containsKey(score) + && !player.settings().collectedRewards().contains(rewardKey)) { + for (final Reward reward : Rewards.oneTimeRewards().get(score)) { + reward.execute(player, mode); + } + player.settings().collectedRewards().add(rewardKey); + player.save(false); } - for (final Reward reward : Rewards.oneTimeRewards().get(score)) { - reward.execute(player, mode); - } - player.settings().collectedRewards().add(rewardKey); - player.save(false); } private void processFall() { diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java index 72a1d6d..d9eaee1 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java @@ -39,17 +39,13 @@ public class GeneratorCleanup { } final int minStandingIndex = findMinStandingIndex(); - if (minStandingIndex == Integer.MAX_VALUE) { - return; + if (minStandingIndex != Integer.MAX_VALUE) { + final int keepBehind = Math.max(0, OptionGeneration.trailKeepBehind()); + final int removeCount = minStandingIndex - keepBehind; + if (removeCount > 0) { + clearHistoryHead(history, removeCount); + } } - - final int keepBehind = Math.max(0, OptionGeneration.trailKeepBehind()); - final int removeCount = minStandingIndex - keepBehind; - if (removeCount <= 0) { - return; - } - - clearHistoryHead(history, removeCount); } private int findMinStandingIndex() { diff --git a/src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java b/src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java index 58e6d29..add31fe 100644 --- a/src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java +++ b/src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java @@ -105,12 +105,11 @@ public class GhostManager { final File file = new File(playerFolder, data.getTimestamp() + FILE_EXTENSION); try { data.saveToFile(file); + evictOldRuns(playerFolder); } catch (IOException e) { LoParkour.getPlugin().getLogger().severe( "Failed to save ghost run for " + data.getPlayerName() + ": " + e.getMessage()); - return; } - evictOldRuns(playerFolder); } /** Max runs kept per player; read from config each call so reloads apply. */ diff --git a/src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java b/src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java index ad02af8..043aef1 100644 --- a/src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java +++ b/src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java @@ -37,16 +37,11 @@ public class GhostRecorder { } public void recordFrame(@NotNull Location currentLocation) { - if (!recording) { - return; + if (recording && lastLocation.distance(currentLocation) >= MIN_MOVEMENT) { + final long timestamp = System.currentTimeMillis() - startTime; + frames.add(new GhostFrame(timestamp, currentLocation.clone())); + lastLocation = currentLocation.clone(); } - if (lastLocation.distance(currentLocation) < MIN_MOVEMENT) { - return; - } - - final long timestamp = System.currentTimeMillis() - startTime; - frames.add(new GhostFrame(timestamp, currentLocation.clone())); - lastLocation = currentLocation.clone(); } /** diff --git a/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java b/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java index 8768fb0..29faead 100644 --- a/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java +++ b/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java @@ -57,24 +57,24 @@ public class GhostReplay { public void start() { if (run.getFrames().isEmpty()) { onFinish.run(); - return; + } else { + final ParkourUser user = ParkourUser.getUser(viewer); + if (user != null) { + ParkourUser.leave(viewer); + } + final Location start = run.getFrames().get(FIRST_FRAME).toLocation(world); + viewer.teleport(start); + moved = true; + ghost = new PlayerGhost(viewer, run.getPlayerName()); + if (ghost.spawn(start)) { + startTime = System.currentTimeMillis(); + frameIndex = FIRST_FRAME; + current = start; + task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, TICK_DELAY, TICK_PERIOD); + } else { + finish(); + } } - final ParkourUser user = ParkourUser.getUser(viewer); - if (user != null) { - ParkourUser.leave(viewer); - } - final Location start = run.getFrames().get(FIRST_FRAME).toLocation(world); - viewer.teleport(start); - moved = true; - ghost = new PlayerGhost(viewer, run.getPlayerName()); - if (!ghost.spawn(start)) { - finish(); - return; - } - startTime = System.currentTimeMillis(); - frameIndex = FIRST_FRAME; - current = start; - task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, TICK_DELAY, TICK_PERIOD); } private void tick() { diff --git a/src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java b/src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java index 9d031b6..9eb3d42 100644 --- a/src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java +++ b/src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java @@ -22,7 +22,7 @@ public final class HoloHook { public static void init() { try { Class.forName("me.filoghost.holographicdisplays.api.HolographicDisplaysAPI"); - } catch (Exception ex) { + } catch (ClassNotFoundException | LinkageError ex) { LoParkour.getPlugin().getLogger().warning("##"); LoParkour.getPlugin().getLogger().warning("## LoParkour only supports Holographic Displays v3.0.0 or higher!"); LoParkour.getPlugin().getLogger().warning("## This hook will now be disabled."); @@ -30,50 +30,47 @@ public final class HoloHook { return; } - HolographicDisplaysAPI.get(LoParkour.getPlugin()).registerGlobalPlaceholder("ip_leaderboard", PLACEHOLDER_REFRESH_TICKS, argument -> { + HolographicDisplaysAPI.get(LoParkour.getPlugin()) + .registerGlobalPlaceholder("ip_leaderboard", PLACEHOLDER_REFRESH_TICKS, HoloHook::resolveLeaderboard); + } - if (argument == null) { - return "?"; - } + /** Resolves the {@code {ip_leaderboard: mode, type, #rank}} global placeholder. */ + private static String resolveLeaderboard(String argument) { + if (argument == null) { + return "?"; + } - // {ip_leaderboard: default, score, #1} - final String[] split = argument.replace(" ", "").split(","); + // {ip_leaderboard: default, score, #1} + final String[] split = argument.replace(" ", "").split(","); + final Mode mode = Registry.getMode(split[0].toLowerCase()); + final Leaderboard leaderboard = mode == null ? null : mode.getLeaderboard(); + if (leaderboard == null) { + return "?"; + } - final Mode mode = Registry.getMode(split[0].toLowerCase()); + final String type = split[1].toLowerCase(); + final String rank = split[2].replace("#", ""); + final Score score = lookupScore(leaderboard, rank); + if (score == null) { + return "?"; + } - if (mode == null) { - return "?"; - } + return switch (type) { + case "score" -> Integer.toString(score.score()); + case "name" -> score.name(); + case "time" -> score.time(); + case "difficulty" -> score.difficulty(); + case "difficulty_string" -> parseDifficulty(score.difficulty()); + default -> "?"; + }; + } - final Leaderboard leaderboard = mode.getLeaderboard(); - - if (leaderboard == null) { - return "?"; - } - - final String type = split[1].toLowerCase(); - final String rank = split[2].replace("#", ""); - - final Score score; - try { - score = leaderboard.getScoreAtRank(Integer.parseInt(rank)); - } catch (NumberFormatException e) { - return "?"; - } - - if (score == null) { - return "?"; - } - - return switch (type) { - case "score" -> Integer.toString(score.score()); - case "name" -> score.name(); - case "time" -> score.time(); - case "difficulty" -> score.difficulty(); - case "difficulty_string" -> parseDifficulty(score.difficulty()); - default -> "?"; - }; - }); + private static Score lookupScore(Leaderboard leaderboard, String rank) { + try { + return leaderboard.getScoreAtRank(Integer.parseInt(rank)); + } catch (NumberFormatException e) { + return null; + } } private static String parseDifficulty(String string) { diff --git a/src/main/java/dev/loki/loparkour/hook/papi/resolver/GlobalPlaceholderResolver.java b/src/main/java/dev/loki/loparkour/hook/papi/resolver/GlobalPlaceholderResolver.java index 3940eb6..7565e88 100644 --- a/src/main/java/dev/loki/loparkour/hook/papi/resolver/GlobalPlaceholderResolver.java +++ b/src/main/java/dev/loki/loparkour/hook/papi/resolver/GlobalPlaceholderResolver.java @@ -4,11 +4,23 @@ import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.leaderboard.model.Score; import dev.loki.loparkour.mode.base.Modes; +import java.util.List; +import java.util.function.Function; + public class GlobalPlaceholderResolver { private final ScoreRankResolver rankResolver = new ScoreRankResolver(); private final PlaceholderFormatters formatters = new PlaceholderFormatters(); + /** Maps a placeholder token to the value extracted from the ranked {@link Score} (checked in declaration order). */ + private final List rankedMappings = List.of( + new RankedMapping("player_rank_", Score::name), + new RankedMapping("score_rank_", Score::score), + new RankedMapping("time_rank_", Score::time), + new RankedMapping("difficulty_rank_", Score::difficulty), + new RankedMapping("difficulty_string_rank_", formatters::difficultyString) + ); + public String resolve(String params) { final String value = resolveStatic(params); if (value != null) { @@ -37,22 +49,14 @@ public class GlobalPlaceholderResolver { } private String resolveRanked(String params) { - if (params.contains("player_rank_")) { - return rankResolver.resolve(params.replace("player_rank_", ""), Score::name); - } - if (params.contains("score_rank_")) { - return rankResolver.resolve(params.replace("score_rank_", ""), Score::score); - } - if (params.contains("time_rank_")) { - return rankResolver.resolve(params.replace("time_rank_", ""), Score::time); - } - if (params.contains("difficulty_rank_")) { - return rankResolver.resolve(params.replace("difficulty_rank_", ""), Score::difficulty); - } - if (params.contains("difficulty_string_rank_")) { - return rankResolver.resolve(params.replace("difficulty_string_rank_", ""), - formatters::difficultyString); + for (final RankedMapping mapping : rankedMappings) { + if (params.contains(mapping.token())) { + return rankResolver.resolve(params.replace(mapping.token(), ""), mapping.extractor()); + } } return null; } + + private record RankedMapping(String token, Function extractor) { + } } diff --git a/src/main/java/dev/loki/loparkour/hook/papi/resolver/PlayerPlaceholderResolver.java b/src/main/java/dev/loki/loparkour/hook/papi/resolver/PlayerPlaceholderResolver.java index 060664f..7f74789 100644 --- a/src/main/java/dev/loki/loparkour/hook/papi/resolver/PlayerPlaceholderResolver.java +++ b/src/main/java/dev/loki/loparkour/hook/papi/resolver/PlayerPlaceholderResolver.java @@ -6,10 +6,29 @@ import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.player.spectator.ParkourSpectator; import org.bukkit.entity.Player; +import java.util.Map; +import java.util.function.Function; + public class PlayerPlaceholderResolver { private final PlaceholderFormatters formatters = new PlaceholderFormatters(); + /** In-session placeholders resolved straight from the player's generator/settings. */ + private final Map> generatorPlaceholders = Map.ofEntries( + Map.entry("score", PlayerPlaceholderResolver::currentScore), + Map.entry("current_score", PlayerPlaceholderResolver::currentScore), + Map.entry("time", PlayerPlaceholderResolver::currentTime), + Map.entry("current_time", PlayerPlaceholderResolver::currentTime), + Map.entry("blocklead", pp -> Integer.toString(pp.settings().blockLead())), + Map.entry("lead", pp -> Integer.toString(pp.settings().blockLead())), + Map.entry("style", pp -> pp.settings().style()), + Map.entry("time_pref", pp -> Integer.toString(pp.settings().selectedTime())), + Map.entry("time_preference", pp -> Integer.toString(pp.settings().selectedTime())), + Map.entry("scoreboard", pp -> pp.settings().showScoreboard().toString()), + Map.entry("difficulty", pp -> Double.toString(pp.settings().schematicDifficulty())), + Map.entry("difficulty_string", pp -> formatters.parseDifficulty(pp.settings().schematicDifficulty())) + ); + public String resolve(Player player, String params) { final String value = resolveLeaderboard(player, params); if (value != null) { @@ -55,35 +74,19 @@ public class PlayerPlaceholderResolver { } private String resolveGenerator(ParkourPlayer pp, String params) { - switch (params) { - case "score", "current_score" -> { - return Integer.toString(pp.session.generator().state.score()); - } - case "time", "current_time" -> { - return pp.session.generator().getFormattedTime(); - } - case "blocklead", "lead" -> { - return Integer.toString(pp.settings().blockLead()); - } - case "style" -> { - return pp.settings().style(); - } - case "time_pref", "time_preference" -> { - return Integer.toString(pp.settings().selectedTime()); - } - case "scoreboard" -> { - return pp.settings().showScoreboard().toString(); - } - case "difficulty" -> { - return Double.toString(pp.settings().schematicDifficulty()); - } - case "difficulty_string" -> { - return formatters.parseDifficulty(pp.settings().schematicDifficulty()); - } - default -> { - return resolveScoreUntil(pp, params); - } + final Function handler = generatorPlaceholders.get(params); + if (handler != null) { + return handler.apply(pp); } + return resolveScoreUntil(pp, params); + } + + private static String currentScore(ParkourPlayer pp) { + return Integer.toString(pp.session.generator().state.score()); + } + + private static String currentTime(ParkourPlayer pp) { + return pp.session.generator().getFormattedTime(); } private String resolveScoreUntil(ParkourPlayer pp, String params) { diff --git a/src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java b/src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java index 230c1e0..4c65380 100644 --- a/src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java +++ b/src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java @@ -24,16 +24,20 @@ public final class VaultHook { } if (economy == null) { - final RegisteredServiceProvider service = Bukkit.getServicesManager().getRegistration(Economy.class); - - if (service == null) { - LoParkour.getPlugin().getLogger().severe("Error while trying to fetch the Vault economy - No economy found"); - return; - } - - economy = service.getProvider(); + economy = resolveEconomy(); } - economy.depositPlayer(player, amount); + if (economy != null) { + economy.depositPlayer(player, amount); + } + } + + private static Economy resolveEconomy() { + final RegisteredServiceProvider service = Bukkit.getServicesManager().getRegistration(Economy.class); + if (service == null) { + LoParkour.getPlugin().getLogger().severe("Error while trying to fetch the Vault economy - No economy found"); + return null; + } + return service.getProvider(); } } diff --git a/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java b/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java index 1339c8c..09b3c3d 100644 --- a/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java +++ b/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java @@ -7,7 +7,6 @@ import dev.loki.loparkour.menu.core.Menus; import dev.loki.loparkour.menu.core.screen.ParkourOption; import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.player.core.ParkourUser; -import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.player.service.UserRegistry; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; @@ -27,53 +26,38 @@ import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; +import java.util.List; + /** Hotbar interactions, chat routing, focus mode and general restrictions for parkour players. */ public class ParkourRestrictionListener implements Listener { @EventHandler public void onChat(AsyncPlayerChatEvent event) { - if (!OptionGeneral.optionsEnabled().getOrDefault(ParkourOption.CHAT, true)) { - return; - } - final Player player = event.getPlayer(); final ParkourUser user = ParkourUser.getUser(player); - if (user == null) { - return; - } - - final Session session = user.session; - if (session.isMuted(user)) { - return; - } - - event.setCancelled(true); - switch (user.chatType()) { - case LOBBY_ONLY -> session.getUsers().forEach( - u -> u.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage())); - case PLAYERS_ONLY -> session.getPlayers().forEach( - u -> u.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage())); - default -> event.setCancelled(false); + if (OptionGeneral.optionsEnabled().getOrDefault(ParkourOption.CHAT, true) + && user != null && !user.session.isMuted(user)) { + event.setCancelled(true); + switch (user.chatType()) { + case LOBBY_ONLY -> user.session.getUsers().forEach( + u -> u.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage())); + case PLAYERS_ONLY -> user.session.getPlayers().forEach( + u -> u.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage())); + default -> event.setCancelled(false); + } } } @EventHandler(priority = EventPriority.HIGHEST) public void onCommand(PlayerCommandPreprocessEvent event) { - if (!Config.CONFIG.getBoolean("focus-mode.enabled")) { - return; - } final ParkourUser user = ParkourUser.getUser(event.getPlayer()); - if (user == null) { - return; - } - final String cmd = event.getMessage().toLowerCase(); - if (Config.CONFIG.getStringList("focus-mode.whitelist").stream().anyMatch(cmd::contains)) { - return; + if (Config.CONFIG.getBoolean("focus-mode.enabled") + && user != null + && Config.CONFIG.getStringList("focus-mode.whitelist").stream().noneMatch(cmd::contains)) { + user.sendTranslated("other.no_do"); + event.setCancelled(true); } - - user.sendTranslated("other.no_do"); - event.setCancelled(true); } @EventHandler(priority = EventPriority.HIGHEST) @@ -93,59 +77,57 @@ public class ParkourRestrictionListener implements Listener { && isContainer && event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND) { event.setCancelled(true); player.closeInventory(); - return; + } else { + handleRightClick(event, player); } + } + private void handleRightClick(PlayerInteractEvent event, Player player) { final boolean isRightClick = (event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_AIR || event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK) && event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND; - if (!isRightClick) { - return; - } - final ItemStack held = heldItem(player); - event.setCancelled(true); - - if (!openHotbarMenu(player, held) && !Config.CONFIG.getBoolean("options.disable-inventory-blocks")) { - event.setCancelled(false); + if (isRightClick) { + final ItemStack held = heldItem(player); + event.setCancelled(true); + if (!openHotbarMenu(player, held) && !Config.CONFIG.getBoolean("options.disable-inventory-blocks")) { + event.setCancelled(false); + } } } private boolean openHotbarMenu(Player player, ItemStack held) { - if (matchesLocaleHotbar(player, "play.item", held)) { - Menus.PLAY.open(player); - return true; - } - if (matchesLocaleHotbar(player, "community.item", held)) { - Menus.COMMUNITY.open(player); - return true; - } - if (matchesLocaleHotbar(player, "settings.item", held)) { - Menus.SETTINGS.open(player); - return true; - } - if (matchesLocaleHotbar(player, "lobby.item", held)) { - Menus.LOBBY.open(player); - return true; - } - if (matchesLocaleHotbar(player, "other.quit", held)) { - ParkourUser.leave(player); - return true; + for (final HotbarAction action : HOTBAR_ACTIONS) { + if (matchesLocaleHotbar(player, action.localeKey(), held)) { + action.apply(player); + return true; + } } return false; } + /** A hotbar item (matched by locale) and the action it triggers, checked in declaration order. */ + private record HotbarAction(String localeKey, java.util.function.Consumer action) { + void apply(Player player) { + action.accept(player); + } + } + + private static final List HOTBAR_ACTIONS = List.of( + new HotbarAction("play.item", p -> Menus.PLAY.open(p)), + new HotbarAction("community.item", p -> Menus.COMMUNITY.open(p)), + new HotbarAction("settings.item", p -> Menus.SETTINGS.open(p)), + new HotbarAction("lobby.item", p -> Menus.LOBBY.open(p)), + new HotbarAction("other.quit", ParkourUser::leave) + ); + @EventHandler(priority = EventPriority.LOW) public void onInventoryClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; + if (event.getWhoClicked() instanceof Player player) { + final InventoryType type = event.getInventory().getType(); + if (type != InventoryType.CHEST && type != InventoryType.CRAFTING) { + restrict(player, event); + } } - if (event.getInventory().getType() == InventoryType.CHEST) { - return; // menu GUI - } - if (event.getInventory().getType() == InventoryType.CRAFTING) { - return; // own inv - } - restrict(player, event); } @EventHandler diff --git a/src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java b/src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java index 2669066..aef2981 100644 --- a/src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java +++ b/src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java @@ -34,24 +34,22 @@ public class PlayerConnectionListener implements Listener { if (Config.CONFIG.getBoolean("bungeecord.enabled")) { Modes.defaultMode().create(player); - return; - } - - if (!player.getWorld().equals(World.getWorld())) { - return; + } else if (player.getWorld().equals(World.getWorld())) { + teleportOutOfParkourWorld(player); } + } + private void teleportOutOfParkourWorld(Player player) { final org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back")); if (fallback != null) { PaperLib.teleportAsync(player, fallback.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); - return; + } else { + PaperLib.teleportAsync(player, Bukkit.getWorlds().stream() + .filter(w -> !w.equals(World.getWorld())) + .findAny() + .orElseThrow(() -> new NoSuchElementException("No fallback world found!")) + .getSpawnLocation()); } - - PaperLib.teleportAsync(player, Bukkit.getWorlds().stream() - .filter(w -> !w.equals(World.getWorld())) - .findAny() - .orElseThrow(() -> new NoSuchElementException("No fallback world found!")) - .getSpawnLocation()); } @EventHandler diff --git a/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java b/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java index c33658f..011c6e0 100644 --- a/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java +++ b/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java @@ -23,18 +23,11 @@ public class SchematicWandListener implements Listener { @EventHandler(ignoreCancelled = true) public void onInteract(@NotNull PlayerInteractEvent event) { - if (event.getHand() != EquipmentSlot.HAND) { - return; - } - if (!SchematicCommandHandler.isWand(event.getItem())) { - return; - } - final Action action = event.getAction(); - if (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) { - return; - } - if (event.getClickedBlock() == null) { + if (event.getHand() != EquipmentSlot.HAND + || !SchematicCommandHandler.isWand(event.getItem()) + || (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) + || event.getClickedBlock() == null) { return; } diff --git a/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java b/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java index 1d9d177..ecd3797 100644 --- a/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java @@ -107,17 +107,16 @@ public class SingleLeaderboardMenu extends LPMenu { private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull UUID id, int rank) { - if (rank > SKULL_RANK_LIMIT || ParkourUser.isBedrockPlayer(player)) { - return; - } final OfflinePlayer op = org.bukkit.Bukkit.getOfflinePlayer(id); - if (op.getName() == null || op.getName().startsWith(".")) { - return; - } - final SkullMeta meta = (SkullMeta) skull.getItemMeta(); - if (meta != null) { - meta.setOwningPlayer(op); - skull.setItemMeta(meta); + if (rank <= SKULL_RANK_LIMIT + && !ParkourUser.isBedrockPlayer(player) + && op.getName() != null + && !op.getName().startsWith(".")) { + final SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(op); + skull.setItemMeta(meta); + } } } diff --git a/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java b/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java index e67dcd1..a6ff0f0 100644 --- a/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java +++ b/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java @@ -39,26 +39,22 @@ public final class GuiManager implements Listener { @EventHandler public void onInventoryClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; + if (event.getWhoClicked() instanceof Player player) { + final OpenGui open = ACTIVE.get(player.getUniqueId()); + if (open != null && open.inventory().equals(event.getInventory())) { + open.gui().handleClick(event); + } } - final OpenGui open = ACTIVE.get(player.getUniqueId()); - if (open == null || !open.inventory().equals(event.getInventory())) { - return; - } - open.gui().handleClick(event); } @EventHandler public void onInventoryClose(InventoryCloseEvent event) { - if (!(event.getPlayer() instanceof Player player)) { - return; + if (event.getPlayer() instanceof Player player) { + final OpenGui open = ACTIVE.get(player.getUniqueId()); + if (open != null && open.inventory().equals(event.getInventory())) { + ACTIVE.remove(player.getUniqueId()); + } } - final OpenGui open = ACTIVE.get(player.getUniqueId()); - if (open == null || !open.inventory().equals(event.getInventory())) { - return; - } - ACTIVE.remove(player.getUniqueId()); } @EventHandler diff --git a/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java b/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java index d6d4e70..9b5218e 100644 --- a/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java @@ -108,16 +108,13 @@ public class DuelsMenu extends LPMenu { } private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser owner) { - if (ParkourUser.isBedrockPlayer(player)) { - return; - } - if (owner.getName() == null || owner.getName().startsWith(".")) { - return; - } - final SkullMeta meta = (SkullMeta) skull.getItemMeta(); - if (meta != null) { - meta.setOwningPlayer(owner.player); - skull.setItemMeta(meta); + final String name = owner.getName(); + if (!ParkourUser.isBedrockPlayer(player) && name != null && !name.startsWith(".")) { + final SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(owner.player); + skull.setItemMeta(meta); + } } } } diff --git a/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java b/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java index 6990ab6..dabc6ee 100644 --- a/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java @@ -84,16 +84,13 @@ public class SpectatorMenu extends LPMenu { } private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser pp) { - if (ParkourUser.isBedrockPlayer(player)) { - return; - } - if (pp.getName() == null || pp.getName().startsWith(".")) { - return; - } - final SkullMeta meta = (SkullMeta) skull.getItemMeta(); - if (meta != null) { - meta.setOwningPlayer(pp.player); - skull.setItemMeta(meta); + final String name = pp.getName(); + if (!ParkourUser.isBedrockPlayer(player) && name != null && !name.startsWith(".")) { + final SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(pp.player); + skull.setItemMeta(meta); + } } } } diff --git a/src/main/java/dev/loki/loparkour/mode/impl/barrier/InvisibleBarrierMode.java b/src/main/java/dev/loki/loparkour/mode/impl/barrier/InvisibleBarrierMode.java index 68b11c0..bd41bf5 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/barrier/InvisibleBarrierMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/barrier/InvisibleBarrierMode.java @@ -41,11 +41,9 @@ public class InvisibleBarrierMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() instanceof BarrierGenerator) { - return; + if (!(pp != null && pp.session.generator() instanceof BarrierGenerator)) { + player.closeInventory(); + Session.create(session -> new BarrierGenerator(session), null, null, player); } - - player.closeInventory(); - Session.create(session -> new BarrierGenerator(session), null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java b/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java index b319ec3..dbd73fe 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java @@ -53,37 +53,29 @@ public class CoopMode implements MultiMode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() instanceof CoopGenerator) { - return; + if (!(pp != null && pp.session.generator() instanceof CoopGenerator)) { + player.closeInventory(); + Session.create(CoopGenerator::new, + session -> session.getPlayers().size() < getMaxPlayers(), + null, + player); + Menus.LOBBY.open(player); } - - player.closeInventory(); - Session.create(CoopGenerator::new, - session -> session.getPlayers().size() < getMaxPlayers(), - null, - player); - Menus.LOBBY.open(player); } @Override public void join(Player joiningPlayer, Session session) { - if (!ModeMessages.checkJoiningEnabled(joiningPlayer)) { - return; - } + if (ModeMessages.checkJoiningEnabled(joiningPlayer) && session.generator() != null) { + final ParkourPlayer pp = ParkourUser.register(joiningPlayer, session); + session.addPlayers(pp); + pp.updateGeneratorSettings(session.generator()); + if (session.generator() instanceof CoopGenerator coopGenerator) { + coopGenerator.onPlayerJoin(pp); + } - if (session.generator() == null) { - return; + session.getPlayers().forEach(p -> + p.sendTranslated("modes.coop.player_joined", joiningPlayer.getName())); } - - final ParkourPlayer pp = ParkourUser.register(joiningPlayer, session); - session.addPlayers(pp); - pp.updateGeneratorSettings(session.generator()); - if (session.generator() instanceof CoopGenerator coopGenerator) { - coopGenerator.onPlayerJoin(pp); - } - - session.getPlayers().forEach(p -> - p.sendTranslated("modes.coop.player_joined", joiningPlayer.getName())); } @Override diff --git a/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java b/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java index 7c3f332..79e656f 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java @@ -66,12 +66,10 @@ public class RaceMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() instanceof RaceGenerator) { - return; + if (!(pp != null && pp.session.generator() instanceof RaceGenerator)) { + player.closeInventory(); + Session.create(session -> new RaceGenerator(session), null, null, player); } - - player.closeInventory(); - Session.create(session -> new RaceGenerator(session), null, null, player); } // ── RaceGenerator ───────────────────────────────────────────────────────── diff --git a/src/main/java/dev/loki/loparkour/mode/impl/multi/SpectatorMode.java b/src/main/java/dev/loki/loparkour/mode/impl/multi/SpectatorMode.java index bb6d892..c0b480d 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/multi/SpectatorMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/multi/SpectatorMode.java @@ -45,23 +45,18 @@ public class SpectatorMode implements Mode { } if (!ParkourOption.SPECTATOR.mayPerform(player)) { player.sendMessage(Locales.getString(player, "other.no_do")); - return; + } else if (!session.getPlayers().isEmpty()) { + final ParkourUser user = ParkourUser.getUser(player); + final ParkourSpectator spectator; + + if (user != null) { + ParkourUser.unregister(user, false, false, false); + spectator = new ParkourSpectator(player, session, user.previousData()); + } else { + spectator = new ParkourSpectator(player, session, null); + } + + session.addSpectators(spectator); } - - final ParkourUser user = ParkourUser.getUser(player); - final ParkourSpectator spectator; - - if (session.getPlayers().isEmpty()) { - return; - } - - if (user != null) { - ParkourUser.unregister(user, false, false, false); - spectator = new ParkourSpectator(player, session, user.previousData()); - } else { - spectator = new ParkourSpectator(player, session, null); - } - - session.addSpectators(spectator); } } diff --git a/src/main/java/dev/loki/loparkour/mode/impl/solo/DefaultMode.java b/src/main/java/dev/loki/loparkour/mode/impl/solo/DefaultMode.java index 71eeec4..0403042 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/solo/DefaultMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/solo/DefaultMode.java @@ -45,12 +45,11 @@ public class DefaultMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof DefaultMode) { - return; + final boolean alreadyPlaying = pp != null && pp.session.generator() != null + && pp.session.generator().getMode() instanceof DefaultMode; + if (!alreadyPlaying) { + player.closeInventory(); + Session.create(ParkourGenerator::new, null, null, player); } - - player.closeInventory(); - - Session.create(ParkourGenerator::new, null, null, player); } } diff --git a/src/main/java/dev/loki/loparkour/mode/impl/solo/GravityShiftMode.java b/src/main/java/dev/loki/loparkour/mode/impl/solo/GravityShiftMode.java index 27f2044..c1dd7a8 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/solo/GravityShiftMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/solo/GravityShiftMode.java @@ -58,12 +58,10 @@ public class GravityShiftMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() instanceof GravityShiftGenerator) { - return; + if (!(pp != null && pp.session.generator() instanceof GravityShiftGenerator)) { + player.closeInventory(); + Session.create(session -> new GravityShiftGenerator(session), null, null, player); } - - player.closeInventory(); - Session.create(session -> new GravityShiftGenerator(session), null, null, player); } // ── GravityShiftGenerator ───────────────────────────────────────────────── diff --git a/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java b/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java index f0365be..23a6c2a 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java @@ -69,12 +69,10 @@ public class SpeedrunMode implements Mode { } final ParkourPlayer pp = UserRegistry.getPlayer(player); - if (pp != null && pp.session.generator() instanceof SpeedrunGenerator) { - return; + if (!(pp != null && pp.session.generator() instanceof SpeedrunGenerator)) { + player.closeInventory(); + Session.create(session -> new SpeedrunGenerator(session), null, null, player); } - - player.closeInventory(); - Session.create(session -> new SpeedrunGenerator(session), null, null, player); } // ── SpeedrunGenerator ───────────────────────────────────────────────────── @@ -106,19 +104,12 @@ public class SpeedrunMode implements Mode { // The block the player just landed on is the previous-to-last in history // (history.size()-1 is the leading block, size()-2 is where player stands) final int histSize = state.history.size(); - if (histSize < 2) { - return; + if (histSize >= 2) { + final Block stepped = state.history.get(histSize - 2); + if (stepped.getType() != Material.AIR && !scheduledTasks.containsKey(stepped)) { + scheduleBlockRemoval(stepped); + } } - - final Block stepped = state.history.get(histSize - 2); - if (stepped.getType() == Material.AIR) { - return; - } - if (scheduledTasks.containsKey(stepped)) { - return; - } - - scheduleBlockRemoval(stepped); } private void scheduleBlockRemoval(@NotNull Block block) { diff --git a/src/main/java/dev/loki/loparkour/player/data/InventoryData.java b/src/main/java/dev/loki/loparkour/player/data/InventoryData.java index 5499caf..ea359ee 100644 --- a/src/main/java/dev/loki/loparkour/player/data/InventoryData.java +++ b/src/main/java/dev/loki/loparkour/player/data/InventoryData.java @@ -57,24 +57,30 @@ public class InventoryData { } private void loadFile(Consumer<@Nullable InventoryData> onFinish) { + if (!tryLoadYaml(onFinish)) { + if (file.length() == 0) { + onFinish.accept(null); + } else { + loadLegacy(onFinish); + } + } + } + + /** Attempts to load modern YAML data, invoking {@code onFinish} and returning {@code true} on success. */ + private boolean tryLoadYaml(Consumer<@Nullable InventoryData> onFinish) { try { final YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); if (!yaml.getKeys(false).isEmpty()) { loadFromYaml(yaml); onFinish.accept(this); - return; + return true; } } catch (Exception ex) { + // Boundary: corrupt player data falls back to legacy format, never breaks join. LoParkour.getPlugin().getLogger().warning( "YAML inventory parse failed for %s, trying legacy format: %s".formatted(file.getName(), ex.getMessage())); } - - if (file.length() == 0) { - onFinish.accept(null); - return; - } - - loadLegacy(onFinish); + return false; } private void loadFromYaml(YamlConfiguration yaml) { diff --git a/src/main/java/dev/loki/loparkour/player/data/PreviousData.java b/src/main/java/dev/loki/loparkour/player/data/PreviousData.java index c4c267f..60299f3 100644 --- a/src/main/java/dev/loki/loparkour/player/data/PreviousData.java +++ b/src/main/java/dev/loki/loparkour/player/data/PreviousData.java @@ -85,7 +85,8 @@ public class PreviousData { player.resetPlayerWeather(); player.setVelocity(new Vector(0, 0, 0)); player.setFallDistance(0f); - } catch (Exception ex) { // not the best way to do this... too bad! + } catch (Exception ex) { + // Boundary: stat restore must never throw; Bukkit behavior varies across 1.19-26.x. LoParkour.getPlugin().getLogger().severe("Error while recovering stats of %s".formatted(player.getName()) + " - " + ex.getMessage()); } diff --git a/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java b/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java index 0389ac3..baff499 100644 --- a/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java +++ b/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java @@ -75,15 +75,13 @@ public final class UserRegistry { user.unregister(); resetScoreboard(user); } catch (Exception ex) { + // Boundary: listener/scoreboard failures must not break the leave path. handleUnregisterError(user, ex); } if (restorePreviousData && shouldKickToBungee(kickIfBungee)) { BungeeUtil.sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server")); - return; - } - - if (restorePreviousData) { + } else if (restorePreviousData) { restorePreviousState(user, urgent); } } finally { diff --git a/src/main/java/dev/loki/loparkour/player/service/ui/ScoreboardManager.java b/src/main/java/dev/loki/loparkour/player/service/ui/ScoreboardManager.java index 5c62afd..2a1a7bb 100644 --- a/src/main/java/dev/loki/loparkour/player/service/ui/ScoreboardManager.java +++ b/src/main/java/dev/loki/loparkour/player/service/ui/ScoreboardManager.java @@ -134,6 +134,7 @@ public class ScoreboardManager { try { return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, text); } catch (Exception e) { + // Boundary: external PlaceholderAPI may throw on version skew; scoreboard must survive. return text; } } diff --git a/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java b/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java index 7124859..f3feffc 100644 --- a/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java +++ b/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java @@ -81,23 +81,17 @@ public class ParkourSpectator extends ParkourUser { player.setGameMode(GameMode.SPECTATOR); updateScoreboard(session.generator()); - // spectator is still being teleported to world - if (closest.getLocation().getWorld() != player.getLocation().getWorld()) { - return; + // spectator is still being teleported to world (same world) and far enough from the followed player + final boolean sameWorld = closest.getLocation().getWorld() == player.getLocation().getWorld(); + final boolean farEnough = closest.getLocation().distanceSquared(player.getLocation()) + >= TELEPORT_THRESHOLD * TELEPORT_THRESHOLD; // avoid sqrt + if (sameWorld && farEnough) { + teleport(closest.getLocation()); + if (player.getGameMode() == GameMode.SPECTATOR) { // if player is a spectator + player.setSpectatorTarget(null); + player.setSpectatorTarget(player.getSpectatorTarget()); + } } - - if (closest.getLocation().distanceSquared(player.getLocation()) < TELEPORT_THRESHOLD * TELEPORT_THRESHOLD) { // avoid sqrt - return; - } - - teleport(closest.getLocation()); - if (player.getGameMode() != GameMode.SPECTATOR) { // if player isn't in spectator or is a bedrock player - return; - } - - // if player is a spectator - player.setSpectatorTarget(null); - player.setSpectatorTarget(player.getSpectatorTarget()); } /** diff --git a/src/main/java/dev/loki/loparkour/reward/core/Reward.java b/src/main/java/dev/loki/loparkour/reward/core/Reward.java index cf478d9..4d0c0f3 100644 --- a/src/main/java/dev/loki/loparkour/reward/core/Reward.java +++ b/src/main/java/dev/loki/loparkour/reward/core/Reward.java @@ -26,41 +26,34 @@ public record Reward(@NotNull String string) { return; } - String command = this.string; - final Mode rewardMode = Registry.getModes().stream() .filter(m -> this.string.contains("%s:".formatted(m.getName().toLowerCase()))) .findFirst() .orElse(Modes.defaultMode()); - if (mode != rewardMode) { - return; - } - - command = command.replaceFirst("%s:".formatted(rewardMode.getName().toLowerCase()), ""); - - // check for placeholders - if (command.toLowerCase().contains("%player%")) { - command = command.replaceAll("%player%", player.getName()); + if (mode == rewardMode) { + String command = string.replaceFirst("%s:".formatted(rewardMode.getName().toLowerCase()), ""); + if (command.toLowerCase().contains("%player%")) { + command = command.replaceAll("%player%", player.getName()); + } + applyCommand(player, command); } + } + private void applyCommand(@NotNull ParkourPlayer player, String command) { // check for extra data if (command.toLowerCase().contains("leave:")) { // leave: - command = command.replaceFirst("leave:", ""); - player.previousData().onLeave.add(new Reward(command)); + player.previousData().onLeave.add(new Reward(command.replaceFirst("leave:", ""))); return; } // check for things to perform if (command.toLowerCase().contains("send:")) { - command = command.replaceFirst("send:", ""); - - player.send(command); + player.send(command.replaceFirst("send:", "")); } else if (command.toLowerCase().contains("vault:")) { - command = command.replaceFirst("vault:", ""); - + final String amount = command.replaceFirst("vault:", ""); try { - VaultHook.deposit(player.player, Double.parseDouble(command)); + VaultHook.deposit(player.player, Double.parseDouble(amount)); } catch (NumberFormatException ex) { LoParkour.getPlugin().getLogger().severe( "Error while trying to process Vault reward - check your rewards file for incorrect numbers - " diff --git a/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java b/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java index 8230d98..be8c3b2 100644 --- a/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java +++ b/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java @@ -33,4 +33,9 @@ final class BukkitBackend implements Scheduler.Backend { public ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) { return Bukkit.getScheduler().runTaskTimer(plugin, task, delayTicks, periodTicks)::cancel; } + + @Override + public ScheduledTask runTimerAsync(Runnable task, long delayTicks, long periodTicks) { + return Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delayTicks, periodTicks)::cancel; + } } diff --git a/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java b/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java index 5495159..15516f2 100644 --- a/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java +++ b/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java @@ -28,6 +28,7 @@ final class FoliaBackend implements Scheduler.Backend { private final Method globalRunDelayed; private final Method globalRunAtFixedRate; private final Method asyncRunNow; + private final Method asyncRunAtFixedRate; private final Method taskCancel; FoliaBackend(Plugin plugin) throws ReflectiveOperationException { @@ -40,6 +41,8 @@ final class FoliaBackend implements Scheduler.Backend { this.globalRunAtFixedRate = method(GLOBAL_SCHEDULER_CLASS, "runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class); this.asyncRunNow = method(ASYNC_SCHEDULER_CLASS, "runNow", Plugin.class, Consumer.class); + this.asyncRunAtFixedRate = method(ASYNC_SCHEDULER_CLASS, "runAtFixedRate", + Plugin.class, Consumer.class, long.class, long.class); this.taskCancel = Class.forName(SCHEDULER_PACKAGE + "ScheduledTask").getMethod("cancel"); } @@ -64,6 +67,12 @@ final class FoliaBackend implements Scheduler.Backend { consumer(task), delayTicks, periodTicks)); } + @Override + public ScheduledTask runTimerAsync(Runnable task, long delayTicks, long periodTicks) { + return wrap(invoke(asyncRunAtFixedRate, asyncScheduler, plugin, + consumer(task), delayTicks, periodTicks)); + } + private static Object scheduler(String bukkitGetter) throws ReflectiveOperationException { return Bukkit.class.getMethod(bukkitGetter).invoke(null); } diff --git a/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java b/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java index b65706a..0282ed6 100644 --- a/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java +++ b/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java @@ -87,6 +87,18 @@ public final class Scheduler { return backend.runTimer(task, delayTicks, periodTicks); } + /** + * Runs the task repeatedly on a worker thread after the given delay. + * + * @param task the task to run + * @param delayTicks first run delay in ticks + * @param periodTicks period between runs in ticks + * @return a handle used to cancel the task + */ + public ScheduledTask runTimerAsync(Runnable task, long delayTicks, long periodTicks) { + return backend.runTimerAsync(task, delayTicks, periodTicks); + } + private static Backend createBackend(Plugin plugin) { if (FOLIA) { try { @@ -118,5 +130,7 @@ public final class Scheduler { ScheduledTask runLater(Runnable task, long delayTicks); ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks); + + ScheduledTask runTimerAsync(Runnable task, long delayTicks, long periodTicks); } } diff --git a/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java index 7bef4bd..91d86a5 100644 --- a/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java +++ b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java @@ -81,7 +81,7 @@ public class SessionStateManager { if (LoParkour.getGhostService() != null) { LoParkour.getGhostService().recordPlayers(session.generator()); } - } catch (Throwable t) { + } catch (RuntimeException t) { session.generator().session.getPlayers().forEach(p -> p.player.sendMessage("§c[LoParkour] Tick error: " + t.getMessage())); } diff --git a/src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java b/src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java index 2e784c4..440c86b 100644 --- a/src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java +++ b/src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java @@ -102,11 +102,10 @@ public final class StorageDisk { LoParkour.getPlugin().getLogger().warning( "Player data file for %s is empty or corrupted, using defaults".formatted(player.getName())); PlayerSettingsManager.applyDefaults(player); - return; + } else { + normalizeLegacyKeys(settings); + PlayerSettingsManager.applySettings(player, settings); } - - normalizeLegacyKeys(settings); - PlayerSettingsManager.applySettings(player, settings); } catch (IOException ex) { LoParkour.getPlugin().getLogger().severe( "Error while trying to read disk data of %s".formatted(player.getName()) + " - " + ex.getMessage()); diff --git a/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java index 4095b1e..a05923e 100644 --- a/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java @@ -57,6 +57,7 @@ public class SQLConnectionManager { executeCallbacks(); } catch (Exception ex) { + // Boundary: pool failure taxonomy is not public API; connect must never break enable. handleConnectionError(ex); } } @@ -103,6 +104,7 @@ public class SQLConnectionManager { try { cb.run(); } catch (Exception e) { + // Boundary: one failing callback must not block the remaining on-connect callbacks. LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage()); } } diff --git a/src/main/java/dev/loki/loparkour/util/item/Materials.java b/src/main/java/dev/loki/loparkour/util/item/Materials.java index 40ef060..de751a2 100644 --- a/src/main/java/dev/loki/loparkour/util/item/Materials.java +++ b/src/main/java/dev/loki/loparkour/util/item/Materials.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Locale; +import java.util.function.Supplier; /** * Parses {@link Material} names from config (modern IDs, registry, legacy aliases). @@ -27,46 +28,33 @@ public final class Materials { return null; } final String key = raw.toLowerCase(Locale.ROOT).replace(' ', '_'); - - Material m = Material.matchMaterial(key, false); - if (m != null) { - return m; - } - - m = fromRegistry(key); - if (m != null) { - return m; - } - - m = Material.matchMaterial(raw.toUpperCase(Locale.ROOT), false); - if (m != null) { - return m; - } - final int colon = key.indexOf(':'); - if (colon >= 0) { - final String id = key.substring(colon + 1); - m = Material.matchMaterial(id, false); - if (m != null) { - return m; - } - m = fromRegistry(id); + final String colonId = colon >= 0 ? key.substring(colon + 1) : null; + + return firstNonNull( + () -> Material.matchMaterial(key, false), + () -> fromRegistry(key), + () -> Material.matchMaterial(raw.toUpperCase(Locale.ROOT), false), + () -> colonId == null ? null : firstNonNull( + () -> Material.matchMaterial(colonId, false), + () -> fromRegistry(colonId)), + () -> coloredSlabFallback(key), + () -> Material.matchMaterial(key, true), + () -> Material.getMaterial(raw.toUpperCase(Locale.ROOT)) + ); + } + + /** Returns the first non-null result produced by the candidates, evaluated in order. */ + @Nullable + @SafeVarargs + private static Material firstNonNull(Supplier... candidates) { + for (final Supplier candidate : candidates) { + final Material m = candidate.get(); if (m != null) { return m; } } - - m = coloredSlabFallback(key); - if (m != null) { - return m; - } - - m = Material.matchMaterial(key, true); - if (m != null) { - return m; - } - - return Material.getMaterial(raw.toUpperCase(Locale.ROOT)); + return null; } @Nullable diff --git a/src/main/java/dev/loki/loparkour/world/core/World.java b/src/main/java/dev/loki/loparkour/world/core/World.java index 628bb04..19d0351 100644 --- a/src/main/java/dev/loki/loparkour/world/core/World.java +++ b/src/main/java/dev/loki/loparkour/world/core/World.java @@ -40,10 +40,12 @@ public final class World { final var existingWorld = Bukkit.getWorld(name); - if (!Config.CONFIG.getBoolean("joining")) { - return; + if (Config.CONFIG.getBoolean("joining")) { + createOrReuse(existingWorld); } + } + private static void createOrReuse(org.bukkit.World existingWorld) { if (existingWorld != null) { LoParkour.getPlugin().getLogger().warning( "Crash detected! The parkour world loading twice is not usual behaviour. " + @@ -134,10 +136,12 @@ public final class World { LoParkour.log("Deleting Spigot world"); final File folder = worldFolder(); - if (!folder.exists()) { - return; + if (folder.exists()) { + deleteWorldFolder(folder); } + } + private static void deleteWorldFolder(File folder) { try { Bukkit.unloadWorld(name, false); } catch (UnsupportedOperationException ex) { diff --git a/src/main/resources/docs/SCHEMATICS.md b/src/main/resources/docs/SCHEMATICS.md index 185ce88..d309349 100644 --- a/src/main/resources/docs/SCHEMATICS.md +++ b/src/main/resources/docs/SCHEMATICS.md @@ -25,28 +25,28 @@ Player setting **0.25 (easy)** → only structures with difficulty ≤ 0.25. ## Creating schematics in-game (admin) -1. Get the wand: `/lp schematic wand` -2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`) +1. Get the wand: `/lpk schematic wand` +2. **Left-click** = position 1, **right-click** = position 2 (or use `/lpk schematic pos1` / `pos2`) 3. Save and register: - - `/lp create ` — auto name `parkour-.schem` + YAML entry - - `/lp schematic create ` — same - - `/lp schematic create ` — custom name (letters, numbers, `-`, `_`) + - `/lpk create ` — auto name `parkour-.schem` + YAML entry + - `/lpk schematic create ` — same + - `/lpk schematic create ` — custom name (letters, numbers, `-`, `_`) Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1). -4. `/lp schematic reload` — reload files without restart +4. `/lpk schematic reload` — reload files without restart ## Admin commands | Command | Description | |---------|-------------| -| `/lp schematic wand` | Selection wand | -| `/lp schematic pos1` / `pos2` | Set corners at your feet | -| `/lp create ` | Save selection → `.schem` + YAML | -| `/lp schematic create ` | Save with custom name | -| `/lp schematic list` | List loaded structures | -| `/lp schematic paste ` | Test-paste at your location | -| `/lp schematic reload` | Reload disk files | +| `/lpk schematic wand` | Selection wand | +| `/lpk schematic pos1` / `pos2` | Set corners at your feet | +| `/lpk create ` | Save selection → `.schem` + YAML | +| `/lpk schematic create ` | Save with custom name | +| `/lpk schematic list` | List loaded structures | +| `/lpk schematic paste ` | Test-paste at your location | +| `/lpk schematic reload` | Reload disk files | ## Flow (runtime) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3168329..8c001ad 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,6 +7,6 @@ main: dev.loki.loparkour.LoParkour softdepend: [floodgate, Vault, PlaceholderAPI, Multiverse-Core] commands: loparkour: - aliases: [parkour, lp] + aliases: [parkour, lpk] usage: / [arguments] description: Main command for LoParkour \ No newline at end of file diff --git a/src/main/resources/schematics/schematics.yml b/src/main/resources/schematics/schematics.yml index 16c3fe2..1262dda 100644 --- a/src/main/resources/schematics/schematics.yml +++ b/src/main/resources/schematics/schematics.yml @@ -1,8 +1,8 @@ # -------- Structure difficulty -------- # plugins/LoParkour/schematics/ # .nbt — structure block export -# .schem / .schematic — WorldEdit or /lp create -# In-game: /lp schematic wand → select → /lp create +# .schem / .schematic — WorldEdit or /lpk create +# In-game: /lpk schematic wand → select → /lpk create # Docs: plugins/LoParkour/docs/SCHEMATICS.md # -------- The difficulty of schematics -------- diff --git a/src/test/resources/archunit_store/339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a b/src/test/resources/archunit_store/339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a index a502e5a..e69de29 100644 --- a/src/test/resources/archunit_store/339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a +++ b/src/test/resources/archunit_store/339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a @@ -1,7 +0,0 @@ -Class extends class in (MetricsCacheStore.java:0) -Constructor (dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> calls constructor ()> in (MetricsCacheStore.java:84) -Field has type in (MetricsCacheStore.java:0) -Method calls constructor (dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> in (MetricsCacheStore.java:84) -Method calls method in (MetricsCacheStore.java:81) -Method calls method in (MetricsCacheStore.java:91) -Method calls method in (MetricsCacheStore.java:96) diff --git a/src/test/resources/archunit_store/f1e3c34b-9b02-40e6-83a6-0269e173eb83 b/src/test/resources/archunit_store/f1e3c34b-9b02-40e6-83a6-0269e173eb83 index ffce91d..e69de29 100644 --- a/src/test/resources/archunit_store/f1e3c34b-9b02-40e6-83a6-0269e173eb83 +++ b/src/test/resources/archunit_store/f1e3c34b-9b02-40e6-83a6-0269e173eb83 @@ -1,3 +0,0 @@ -Method calls method in (DuelsCountdown.java:45) -Method calls method in (DuelsCountdown.java:55) -Method calls method in (DuelsGenerator.java:137)