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)
This commit is contained in:
parent
8ff02cff18
commit
b6712391fd
79 changed files with 696 additions and 800 deletions
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
### Новое
|
### Новое
|
||||||
|
|
||||||
- **Ghost: личные реплеи забегов.** Серверный тумблер записи (`ghost-mode.enabled`) + персональный переключатель в меню настроек. Запоминаются последние 5 забегов игрока (`ghost-mode.max-runs`). Просмотр через `/lp replay [номер]`: вас телепортирует на старт записанного забега, и моделька игрока с вашим скином повторяет движение по блокам.
|
- **Ghost: личные реплеи забегов.** Серверный тумблер записи (`ghost-mode.enabled`) + персональный переключатель в меню настроек. Запоминаются последние 5 забегов игрока (`ghost-mode.max-runs`). Просмотр через `/lpk replay [номер]`: вас телепортирует на старт записанного забега, и моделька игрока с вашим скином повторяет движение по блокам.
|
||||||
- **Coop-режим теперь работает.** `/lp join coop` создаёт общую сессию, друзья заходят через `/lp join <ник>`, общий счёт и статистика вклада.
|
- **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+).
|
- **Мультиверсия в 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
|
### 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.
|
- **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.** `/lp join coop` creates a shared session, friends join via `/lp join <name>`, shared score and contribution stats.
|
- **Coop mode now works.** `/lpk join coop` creates a shared session, friends join via `/lpk join <name>`, 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+).
|
- **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
|
### Fixes
|
||||||
|
|
|
||||||
31
README.md
31
README.md
|
|
@ -6,7 +6,6 @@ Advanced infinitely generating parkour plugin with ghost system, leaderboards an
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|
@ -53,14 +52,14 @@ LoParkour is an advanced parkour plugin with infinite world generation. The cour
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/lp join` | Join parkour |
|
| `/lpk join` | Join parkour |
|
||||||
| `/lp leave` | Leave parkour |
|
| `/lpk leave` | Leave parkour |
|
||||||
| `/lp spectate <player>` | Spectate another player |
|
| `/lpk spectate <player>` | Spectate another player |
|
||||||
| `/lp schematic save <name>` | Save a schematic |
|
| `/lpk schematic save <name>` | Save a schematic |
|
||||||
| `/lp schematic reload` | Reload schematics |
|
| `/lpk schematic reload` | Reload schematics |
|
||||||
| `/lp schematic convert` | Convert old vilib format |
|
| `/lpk schematic convert` | Convert old vilib format |
|
||||||
| `/lp forcejoin <player>` | Force player to join (admin) |
|
| `/lpk forcejoin <player>` | Force player to join (admin) |
|
||||||
| `/lp reset` | Reset leaderboards (admin) |
|
| `/lpk reset` | Reset leaderboards (admin) |
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
|
|
@ -103,13 +102,13 @@ LoParkour - продвинутый паркур-плагин с бесконеч
|
||||||
|
|
||||||
| Команда | Описание |
|
| Команда | Описание |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| `/lp join` | Начать паркур |
|
| `/lpk join` | Начать паркур |
|
||||||
| `/lp leave` | Выйти из паркура |
|
| `/lpk leave` | Выйти из паркура |
|
||||||
| `/lp spectate <игрок>` | Наблюдать за игроком |
|
| `/lpk spectate <игрок>` | Наблюдать за игроком |
|
||||||
| `/lp schematic save <имя>` | Сохранить схематику |
|
| `/lpk schematic save <имя>` | Сохранить схематику |
|
||||||
| `/lp schematic reload` | Перезагрузить схематики |
|
| `/lpk schematic reload` | Перезагрузить схематики |
|
||||||
| `/lp forcejoin <игрок>` | Заставить игрока начать (админ) |
|
| `/lpk forcejoin <игрок>` | Заставить игрока начать (админ) |
|
||||||
| `/lp reset` | Сбросить лидерборды (админ) |
|
| `/lpk reset` | Сбросить лидерборды (админ) |
|
||||||
|
|
||||||
### Зависимости
|
### Зависимости
|
||||||
|
|
||||||
|
|
|
||||||
52
TODO.md
52
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) ИЛИ удалить подсистему и конфиг.
|
- **22. Ghost-система — 100% мёртвый код.** `GhostManager`/`GhostRecorder`/`GhostPlayer` нигде не инстанцируются (grep: только само-ссылки). Конфиг `ghost-mode.*` не потребляется. → Подключить к lifecycle (запись в saveGhost/спавн в spawnGhosts) ИЛИ удалить подсистему и конфиг.
|
||||||
- **23. Coop-режим нерабочий.** Ни одно `Session.create` не строит `CoopGenerator` (CoopMode.java:134). `/parkour join <игрок>` ведёт в спектаторы. → Реализовать создание coop-сессии (лобби/инвайты) или выпилить режим из меню и регистрации.
|
- **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).
|
- **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`.
|
- **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`.
|
- **26. ConfigUpdater уничтожает пользовательские данные.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):**
|
||||||
- **27. SQL: утечка Hikari-пулов.** `StorageSQL.init(mode)` вызывается на каждый режим (6 лидербордов), каждый раз пересоздаёт `connectionManager` (StorageSQL.java:66-77, LeaderboardStorage.java:35) → до ~10 соединений висят навсегда. → Один пул на плагин, инициализация один раз.
|
точечный merge (`ConfigMergeProcessor` + `ConfigMergeParser`): block-sequence листы и `ignoredSections` сохраняются,
|
||||||
- **28. SQL: блокирующий connect на main thread** (SQLConnectionManager.java:34-56) — при лежащей БД включение висит до 5с×режимы. → `setInitializationFailTimeout(-1)` + асинхронный connect.
|
есть `ConfigMergeProcessorTest` (249 строк). `performUpdate` ловит только `IOException`, runtime уходит на границу `ConfigLoader`.
|
||||||
- **29. SQL: адаптив не стартует.** `AdaptiveServices.init` вызывается до создания лидербордов, а `StorageSQL.runWhenConnected` выбрасывает колбэк при не-подключении (StorageSQL.java:45-51). → Очередь колбэков в SQLConnectionManager.onConnectCallbacks; порядок init: лидерборды → storage → adaptive.
|
- **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 теперь используют **один и тот же** набор ключей.
|
- **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)` + лимиты размера файла/блоков.
|
- **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 (простой допустимый шаг).
|
- **32. JumpCalculator: возврат невалидного прыжка.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):**
|
||||||
- **33. JumpCalculator: диагональный heading залипает.** `JumpDirector.getRecommendedHeading` в безопасной зоне возвращает current без изменений (JumpDirector.java:105-107), а `calculateJumpOffset` при `offset.getX()!=0` перезаписывает Z (JumpCalculator.java:110-115) → курс зигзагом и не выпрямляется. → При диагонали аппендикс не перезаписывать случайным; корректный поворот-домножитель.
|
`validateAndRetryJump` + `safetyFallback` (3 ре-ролла шага 1/0, затем прямой шаг без джиттера, в крайнем случае `current`);
|
||||||
|
покрыто `JumpCalculatorTest` (`safetyFallbackReturnsValidStepForUnitHeading`, `...ReturnsCurrentWhenStraightStepImpossible`).
|
||||||
|
- **33. JumpCalculator: диагональный heading залипает.** ✅ **СДЕЛАНО (ещё в fea15e6, подтверждено 2026-10-10):**
|
||||||
|
`offsetFor` считает перпендикулярный джиттер без перезаписи Z; покрыто `JumpCalculatorTest`.
|
||||||
|
|
||||||
### Генерация / lifecycle
|
### Генерация / lifecycle
|
||||||
|
|
||||||
|
|
@ -317,18 +330,17 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар
|
||||||
- **136. [M] Починить `IllegalCatch` (legacy, 22 / 19 файлов).** catch(Exception/Throwable/RuntimeException)→сужение до конкретных исключений либо переименовать в `expected|ignore`.
|
- **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)` — комментарий/конкретный тип.
|
- **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`.
|
- **138. [S] Перевести PMD из report-only в жёсткий гейт.** После зачистки убрать `isIgnoreFailures=true` в `build.gradle.kts` (pmd-блок). ✅ сделано в `b771cc1`.
|
||||||
- **169. [M] Починить `ReturnCount` (69 warning / 52 файла) — ПЕРЕСТРУКТУРИРОВАНИЕМ, не порогом.** Все 69 — это половина
|
- **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.
|
||||||
правила про void (`maxForVoid` по умолчанию = 1 ⇒ запрещена любая guard-клауза `if (x) { return; }`); для value-методов
|
- **170. [M] Починить `IllegalCatch` (23 warning / 18 файлов).** ✅ **ЧАСТИЧНО (2026-10-10, остаток 18 — осознанные границы, правило остаётся в warning):**
|
||||||
при `max=5` нарушений 0. Владелец запретил поднимать порог → в каждом из 69 void-методов убираем ранний `return`
|
3× `catch (Throwable)` → `catch (RuntimeException)` (диспетчер команд, session-tick, shutdown — `Error` больше не глотается);
|
||||||
(условие становится обычным `if`-блоком) либо выносим тело в helper-метод. Следить за `NestedIfDepth` (error, max=2)
|
сужено 5: `HoloHook` → `ClassNotFoundException | LinkageError`, `OptionGeneral` → `NumberFormatException` и
|
||||||
и `NPathComplexity`. После обнуления — перевести `ReturnCount` в `error`.
|
`IllegalArgumentException | IndexOutOfBoundsException`, `GeneratorProfileManager` → `IllegalArgumentException`,
|
||||||
- **170. [M] Починить `IllegalCatch` (23 warning / 18 файлов).** 19× `catch (Exception)`, 3× `catch (Throwable)`,
|
`ConfigUpdater` → `IOException` (runtime уходит на границу `ConfigLoader`).
|
||||||
1× `catch (RuntimeException)`. Там, где конкретика реальна, — сужать до `IOException`/`NumberFormatException`/
|
Не сужено осознанно (у каждого — комментарий «почему»): границы enable/reload/shutdown (`PluginBootstrap`,
|
||||||
`SQLException`/`YAMLException` и т. п. На истинных границах (диспетчер команд `LoParkourCommand:36`, `session-state:84`,
|
`ConfigLoader`, `LocaleLoader`, `LoParkour:106`, `SQLConnectionManager:59` — таксономия ошибок HikariCP не публична),
|
||||||
`LoParkour:128`) `Throwable` **обязан** стать `RuntimeException`: `Throwable` глотает `Error` (OOM, StackOverflow) —
|
порча данных (`InventoryData`, `PreviousData`, `SchematicManager` ×2), внешние API (`ScoreboardManager`/PAPI),
|
||||||
это баг, а не стиль. Пограничные `catch (Exception)` в `onEnable/onDisable`, загрузчиках конфигов/схематчей и
|
leave-путь (`UserRegistry`), изоляция колбэков (`SQLConnectionManager:106`), хот-путь генерации
|
||||||
`SchematicManager` — оставляем широкими осознанно (релоад плагина не должен ронять сервер), но каждый такой site
|
(`JumpValidator`, `BlockSelector`), кэш сообщений (`LocaleCache`).
|
||||||
требует комментария «почему именно Exception»; после разбора правило остаётся в warning на незачищенном остатке.
|
|
||||||
|
|
||||||
После зачистки п.132–137 — удалить соответствующие записи из `config/checkstyle/suppressions.xml` (супрессии остаются только для ещё незачищенных legacy-файлов).
|
После зачистки п.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» — просмотреть их список как чек-лист.
|
- **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).
|
→ Вынести версионно-зависимое в один слой `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 — честнее убрать бейдж, если не успеваем.
|
- **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`**; *имя ждёт решения владельца*).
|
→ Убрать `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`).
|
→ Править: `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-сообщение при вызове старого алиаса, если он останется.
|
→ Права (`loparkour.*`) не менять. Добавить в CHANGELOG пометку о breaking-изменении алиаса; рассмотреть deprecation-сообщение при вызове старого алиаса, если он останется.
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
<module name="ReturnCount">
|
<module name="ReturnCount">
|
||||||
<property name="max" value="5"/>
|
<property name="max" value="5"/>
|
||||||
<property name="severity" value="warning"/>
|
<property name="severity" value="error"/>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||
<module name="AvoidStarImport"/>
|
<module name="AvoidStarImport"/>
|
||||||
|
|
|
||||||
|
|
@ -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` |
|
| `.nbt` | Vanilla structure block, `/structure save` | Pasted with Bukkit `StructureManager` |
|
||||||
| `.schem`, `.schematic` | WorldEdit (`//copy`, `//schem save`) | Read via embedded WorldEdit clipboard IO |
|
| `.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).
|
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)
|
## Creating schematics in-game (admin)
|
||||||
|
|
||||||
1. Get the wand: `/lp schematic wand`
|
1. Get the wand: `/lpk schematic wand`
|
||||||
2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`)
|
2. **Left-click** = position 1, **right-click** = position 2 (or use `/lpk schematic pos1` / `pos2`)
|
||||||
3. Save and register:
|
3. Save and register:
|
||||||
- `/lp create <difficulty>` — auto name `parkour-<hash>.schem` + YAML entry
|
- `/lpk create <difficulty>` — auto name `parkour-<hash>.schem` + YAML entry
|
||||||
- `/lp schematic create <difficulty>` — same
|
- `/lpk schematic create <difficulty>` — same
|
||||||
- `/lp schematic create <name> <difficulty>` — custom name (letters, numbers, `-`, `_`)
|
- `/lpk schematic create <name> <difficulty>` — custom name (letters, numbers, `-`, `_`)
|
||||||
|
|
||||||
Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1).
|
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
|
## Converting old `.lpschem` files
|
||||||
|
|
||||||
Place `.lpschem` files in `plugins/LoParkour/schematics/` or legacy `schematics-new/`, then:
|
Place `.lpschem` files in `plugins/LoParkour/schematics/` or legacy `schematics-new/`, then:
|
||||||
|
|
||||||
```
|
```
|
||||||
/lp schematic convert
|
/lpk schematic convert
|
||||||
```
|
```
|
||||||
|
|
||||||
Each file becomes `parkour-<id>.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.
|
Each file becomes `parkour-<id>.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-<id>.schem` in `schematics/` and gets a `difficulty:`
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/lp schematic wand` | Selection wand |
|
| `/lpk schematic wand` | Selection wand |
|
||||||
| `/lp schematic pos1` / `pos2` | Set corners at your feet |
|
| `/lpk schematic pos1` / `pos2` | Set corners at your feet |
|
||||||
| `/lp create <difficulty>` | Save selection → `.schem` + YAML |
|
| `/lpk create <difficulty>` | Save selection → `.schem` + YAML |
|
||||||
| `/lp schematic create <name> <difficulty>` | Save with custom name |
|
| `/lpk schematic create <name> <difficulty>` | Save with custom name |
|
||||||
| `/lp schematic convert` | Convert all `.lpschem` in schematics folders |
|
| `/lpk schematic convert` | Convert all `.lpschem` in schematics folders |
|
||||||
| `/lp schematic list` | List loaded structures |
|
| `/lpk schematic list` | List loaded structures |
|
||||||
| `/lp schematic paste <name>` | Test-paste at your location |
|
| `/lpk schematic paste <name>` | Test-paste at your location |
|
||||||
| `/lp schematic reload` | Reload disk files |
|
| `/lpk schematic reload` | Reload disk files |
|
||||||
|
|
||||||
## Flow (runtime)
|
## Flow (runtime)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ public final class LoParkour extends JavaPlugin {
|
||||||
try {
|
try {
|
||||||
session.onAllPlayersLeft();
|
session.onAllPlayersLeft();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
// Boundary: one failing session must not abort server shutdown.
|
||||||
getLogger().warning("Error cleaning up session: " + e.getMessage());
|
getLogger().warning("Error cleaning up session: " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -125,7 +126,7 @@ public final class LoParkour extends JavaPlugin {
|
||||||
}
|
}
|
||||||
Storage.close();
|
Storage.close();
|
||||||
World.delete();
|
World.delete();
|
||||||
} catch (Throwable t) {
|
} catch (RuntimeException t) {
|
||||||
getLogger().log(Level.SEVERE, "Error during plugin shutdown: " + t.getMessage(), t);
|
getLogger().log(Level.SEVERE, "Error during plugin shutdown: " + t.getMessage(), t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ package dev.loki.loparkour.adaptive.core.metrics;
|
||||||
|
|
||||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
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.bukkit.plugin.Plugin;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.bukkit.scheduler.BukkitRunnable;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
@ -29,7 +30,7 @@ public class MetricsCacheStore {
|
||||||
private final StatsRepository repository;
|
private final StatsRepository repository;
|
||||||
private final Map<UUID, PlayerMetrics> metricsCache;
|
private final Map<UUID, PlayerMetrics> metricsCache;
|
||||||
|
|
||||||
private BukkitRunnable autoSaveTask;
|
private ScheduledTask autoSaveTask;
|
||||||
|
|
||||||
public MetricsCacheStore(@NotNull Plugin plugin, @NotNull StatsRepository repository) {
|
public MetricsCacheStore(@NotNull Plugin plugin, @NotNull StatsRepository repository) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
|
@ -81,14 +82,8 @@ public class MetricsCacheStore {
|
||||||
autoSaveTask.cancel();
|
autoSaveTask.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
autoSaveTask = new BukkitRunnable() {
|
autoSaveTask = Scheduler.get(plugin).runTimerAsync(this::saveAll,
|
||||||
@Override
|
AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS);
|
||||||
public void run() {
|
|
||||||
saveAll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopAutoSave() {
|
public void stopAutoSave() {
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ public final class PluginBootstrap {
|
||||||
World.create();
|
World.create();
|
||||||
plugin.getLogger().info("Parkour world created successfully!");
|
plugin.getLogger().info("Parkour world created successfully!");
|
||||||
} catch (Exception ex) {
|
} 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);
|
plugin.getLogger().log(Level.SEVERE, "Failed to create parkour world", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,11 @@ public final class ForceJoinExecutor {
|
||||||
}
|
}
|
||||||
if ("everyone".equalsIgnoreCase(target)) {
|
if ("everyone".equalsIgnoreCase(target)) {
|
||||||
joinEveryone(sender);
|
joinEveryone(sender);
|
||||||
return;
|
} else if ("nearest".equalsIgnoreCase(target)) {
|
||||||
}
|
|
||||||
if ("nearest".equalsIgnoreCase(target)) {
|
|
||||||
joinNearest(sender);
|
joinNearest(sender);
|
||||||
return;
|
} else {
|
||||||
|
joinByName(target, sender);
|
||||||
}
|
}
|
||||||
joinByName(target, sender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void joinEveryone(CommandSender sender) {
|
private void joinEveryone(CommandSender sender) {
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ public final class ForceLeaveExecutor {
|
||||||
}
|
}
|
||||||
if ("everyone".equalsIgnoreCase(target)) {
|
if ("everyone".equalsIgnoreCase(target)) {
|
||||||
leaveEveryone(sender);
|
leaveEveryone(sender);
|
||||||
return;
|
} else {
|
||||||
|
leaveByName(target, sender);
|
||||||
}
|
}
|
||||||
leaveByName(target, sender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void leaveEveryone(CommandSender sender) {
|
private void leaveEveryone(CommandSender sender) {
|
||||||
|
|
@ -38,15 +38,13 @@ public final class ForceLeaveExecutor {
|
||||||
|
|
||||||
private void leaveByName(String target, CommandSender sender) {
|
private void leaveByName(String target, CommandSender sender) {
|
||||||
final Player other = Bukkit.getPlayer(target);
|
final Player other = Bukkit.getPlayer(target);
|
||||||
|
final ParkourUser user = other == null ? null : ParkourUser.getUser(other);
|
||||||
if (other == null) {
|
if (other == null) {
|
||||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||||
return;
|
} else if (user == null) {
|
||||||
}
|
|
||||||
final ParkourUser user = ParkourUser.getUser(other);
|
|
||||||
if (user == null) {
|
|
||||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing"));
|
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing"));
|
||||||
return;
|
} else {
|
||||||
|
ParkourUser.leave(user);
|
||||||
}
|
}
|
||||||
ParkourUser.leave(user);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ public final class RecoverInventoryExecutor {
|
||||||
final Player other = Bukkit.getPlayer(target);
|
final Player other = Bukkit.getPlayer(target);
|
||||||
if (other == null) {
|
if (other == null) {
|
||||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
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) {
|
private void recover(Player other, CommandSender sender) {
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ public final class ResetExecutor {
|
||||||
}
|
}
|
||||||
if ("everyone".equalsIgnoreCase(target)) {
|
if ("everyone".equalsIgnoreCase(target)) {
|
||||||
resetEveryone(sender);
|
resetEveryone(sender);
|
||||||
return;
|
} else {
|
||||||
|
resetPlayer(target, sender);
|
||||||
}
|
}
|
||||||
resetPlayer(target, sender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetEveryone(CommandSender sender) {
|
private void resetEveryone(CommandSender sender) {
|
||||||
|
|
|
||||||
|
|
@ -54,16 +54,13 @@ class CommandRouter {
|
||||||
private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) {
|
private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) {
|
||||||
if (isAdminCommand(a1)) {
|
if (isAdminCommand(a1)) {
|
||||||
admin.handle(a1, a2, sender, p);
|
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) {
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("schematic".equalsIgnoreCase(a1)) {
|
if ("schematic".equalsIgnoreCase(a1)) {
|
||||||
if ("create".equalsIgnoreCase(a2)) {
|
if ("create".equalsIgnoreCase(a2)) {
|
||||||
SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player);
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
if ("schematic".equalsIgnoreCase(a1) && "create".equalsIgnoreCase(a2)) {
|
||||||
return;
|
SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player);
|
||||||
}
|
}
|
||||||
if (!"schematic".equalsIgnoreCase(a1) || !"create".equalsIgnoreCase(a2)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
final Player p = sender instanceof Player ? (Player) sender : null;
|
final Player p = sender instanceof Player ? (Player) sender : null;
|
||||||
router.dispatch(args, sender, p);
|
router.dispatch(args, sender, p);
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable t) {
|
} catch (RuntimeException t) {
|
||||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
||||||
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
||||||
Locales.send(sender, "commands.error");
|
Locales.send(sender, "commands.error");
|
||||||
|
|
|
||||||
|
|
@ -27,23 +27,21 @@ public class JoinCommandExecutor {
|
||||||
final Mode mode = Registry.getMode(arg);
|
final Mode mode = Registry.getMode(arg);
|
||||||
if (mode != null) {
|
if (mode != null) {
|
||||||
mode.create(p);
|
mode.create(p);
|
||||||
return;
|
} else {
|
||||||
|
joinPlayingPlayer(arg, sender, p);
|
||||||
}
|
}
|
||||||
joinPlayingPlayer(arg, sender, p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void joinPlayingPlayer(String arg, CommandSender sender, Player p) {
|
private void joinPlayingPlayer(String arg, CommandSender sender, Player p) {
|
||||||
final Player other = Bukkit.getPlayer(arg);
|
final Player other = Bukkit.getPlayer(arg);
|
||||||
|
final ParkourPlayer pp = other == null ? null : UserRegistry.getPlayer(other);
|
||||||
if (other == null) {
|
if (other == null) {
|
||||||
CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player"));
|
CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player"));
|
||||||
return;
|
} else if (pp == null) {
|
||||||
}
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(other);
|
|
||||||
if (pp == null) {
|
|
||||||
CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing"));
|
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) {
|
private void joinSession(CommandSender sender, Player p, Session session) {
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,9 @@ public class PlayerCommandHandler {
|
||||||
switch (lower) {
|
switch (lower) {
|
||||||
case "join" -> handleJoin(sender, player);
|
case "join" -> handleJoin(sender, player);
|
||||||
case "leave" -> {
|
case "leave" -> {
|
||||||
if (!cooldown(sender, "leave", COOLDOWN_MS)) {
|
if (cooldown(sender, "leave", COOLDOWN_MS)) {
|
||||||
return;
|
ParkourUser.leave(player);
|
||||||
}
|
}
|
||||||
ParkourUser.leave(player);
|
|
||||||
}
|
}
|
||||||
case "play" -> {
|
case "play" -> {
|
||||||
if (ParkourOption.PLAY.mayPerform(player)) {
|
if (ParkourOption.PLAY.mayPerform(player)) {
|
||||||
|
|
@ -71,19 +70,19 @@ public class PlayerCommandHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "leaderboard" -> {
|
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"));
|
send(sender, Locales.getString(player, "other.no_do"));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
Menus.LEADERBOARDS.open(player);
|
|
||||||
}
|
}
|
||||||
case "replay" -> new ReplayCommandExecutor().replay(null, sender, player, this);
|
case "replay" -> new ReplayCommandExecutor().replay(null, sender, player, this);
|
||||||
case "schematic" -> {
|
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"));
|
send(sender, Locales.getString(player, "other.no_do"));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
SchematicCommandHandler.sendHelp(player);
|
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
}
|
}
|
||||||
|
|
@ -96,12 +95,9 @@ public class PlayerCommandHandler {
|
||||||
}
|
}
|
||||||
if (!ParkourOption.JOIN.mayPerform(player)) {
|
if (!ParkourOption.JOIN.mayPerform(player)) {
|
||||||
send(sender, Locales.getString(player, "other.no_do"));
|
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) {
|
private void handleReload(@NotNull CommandSender sender, @Nullable Player player) {
|
||||||
|
|
@ -110,11 +106,11 @@ public class PlayerCommandHandler {
|
||||||
}
|
}
|
||||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
send(sender, Locales.getString(sender, "other.no_do"));
|
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) {
|
public void sendHelp(@NotNull CommandSender sender) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable;
|
||||||
import java.util.List;
|
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 {
|
public class ReplayCommandExecutor {
|
||||||
|
|
||||||
|
|
@ -30,14 +30,18 @@ public class ReplayCommandExecutor {
|
||||||
final GhostService ghostService = LoParkour.getGhostService();
|
final GhostService ghostService = LoParkour.getGhostService();
|
||||||
if (ghostService == null || !Config.CONFIG.getBoolean("ghost-mode.enabled")) {
|
if (ghostService == null || !Config.CONFIG.getBoolean("ghost-mode.enabled")) {
|
||||||
CommandUtil.send(sender, Locales.getString(sender, "commands.replay.disabled"));
|
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);
|
final GhostData run = resolveRun(indexArg, sender, p, ghostService);
|
||||||
if (run == null) {
|
if (run != null) {
|
||||||
return;
|
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,
|
private static GhostData resolveRun(@Nullable String indexArg, @NotNull CommandSender sender,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,15 @@ final class SchematicHandler {
|
||||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete"));
|
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
createSelected(sel, optionalName, difficultyRaw, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSelected(
|
||||||
|
Location[] sel,
|
||||||
|
@Nullable String optionalName,
|
||||||
|
@NotNull String difficultyRaw,
|
||||||
|
Player player
|
||||||
|
) {
|
||||||
final double difficulty;
|
final double difficulty;
|
||||||
try {
|
try {
|
||||||
difficulty = SchematicCreator.parseDifficulty(difficultyRaw);
|
difficulty = SchematicCreator.parseDifficulty(difficultyRaw);
|
||||||
|
|
@ -60,18 +68,17 @@ final class SchematicHandler {
|
||||||
static void paste(String name, CommandSender sender, Player player) {
|
static void paste(String name, CommandSender sender, Player player) {
|
||||||
if (!isValidSchematicName(name)) {
|
if (!isValidSchematicName(name)) {
|
||||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_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) {
|
static void posCommand(Player player, Location[] existing, Location loc, int index) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ public final class ConfigLoader {
|
||||||
ConfigUpdater.update(configFile, resourceStream, "1.0", ignoredSections, plugin.getLogger());
|
ConfigUpdater.update(configFile, resourceStream, "1.0", ignoredSections, plugin.getLogger());
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} 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,
|
plugin.getLogger().log(java.util.logging.Level.SEVERE,
|
||||||
"Error while trying to update config file: " + fileName, ex);
|
"Error while trying to update config file: " + fileName, ex);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public final class ConfigUpdater {
|
||||||
final List<String> merged = new ConfigMergeProcessor().merge(templateStream, configFile, ignoredKeys);
|
final List<String> merged = new ConfigMergeProcessor().merge(templateStream, configFile, ignoredKeys);
|
||||||
Files.write(configFile.toPath(), merged);
|
Files.write(configFile.toPath(), merged);
|
||||||
logger.info("Configuration file synced: " + configFile.getName());
|
logger.info("Configuration file synced: " + configFile.getName());
|
||||||
} catch (Exception e) {
|
} catch (IOException e) {
|
||||||
if (backup != null && backup.exists()) {
|
if (backup != null && backup.exists()) {
|
||||||
Files.copy(backup.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(backup.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
logger.severe("Config sync failed, restored backup: " + e.getMessage());
|
logger.severe("Config sync failed, restored backup: " + e.getMessage());
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ public class LocaleCache {
|
||||||
final T result = extractor.apply(config);
|
final T result = extractor.apply(config);
|
||||||
return result != null ? result : defaultValue;
|
return result != null ? result : defaultValue;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
// Boundary: message formatting must never throw; locale data is arbitrary.
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ public final class LocaleLoader {
|
||||||
|
|
||||||
return loadFromDisk(embedded);
|
return loadFromDisk(embedded);
|
||||||
} catch (Exception ex) {
|
} 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());
|
plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage());
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ public final class OptionGeneral {
|
||||||
goBackLocation.setYaw(Float.parseFloat(axes[1].trim()));
|
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");
|
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]),
|
return new Location(world, Double.parseDouble(values[0]),
|
||||||
Double.parseDouble(values[1]), Double.parseDouble(values[2]));
|
Double.parseDouble(values[1]), Double.parseDouble(values[2]));
|
||||||
} catch (Exception ex) {
|
} catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
|
||||||
LoParkour.getPlugin().getLogger().warning(
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
"Invalid bungeecord.go-back value '" + location + "'. Falling back to spawn world origin.");
|
"Invalid bungeecord.go-back value '" + location + "'. Falling back to spawn world origin.");
|
||||||
final World fallback = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0);
|
final World fallback = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0);
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ package dev.loki.loparkour.duels;
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.locale.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
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 dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
@ -35,14 +35,14 @@ final class DuelsCountdown {
|
||||||
private static final String DEFAULT_COLOR = "E120";
|
private static final String DEFAULT_COLOR = "E120";
|
||||||
|
|
||||||
private final DuelsGenerator generator;
|
private final DuelsGenerator generator;
|
||||||
private BukkitTask countdownTask;
|
private ScheduledTask countdownTask;
|
||||||
|
|
||||||
DuelsCountdown(@NotNull DuelsGenerator generator) {
|
DuelsCountdown(@NotNull DuelsGenerator generator) {
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
void giveStartItem(@NotNull ParkourPlayer owner) {
|
void giveStartItem(@NotNull ParkourPlayer owner) {
|
||||||
Bukkit.getScheduler().runTaskLater(LoParkour.getPlugin(), () -> {
|
Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
||||||
if (owner.player.isOnline()) {
|
if (owner.player.isOnline()) {
|
||||||
owner.player.getInventory().addItem(Locales.getItem(owner.locale(), "play.multi.duels.start").build());
|
owner.player.getInventory().addItem(Locales.getItem(owner.locale(), "play.multi.duels.start").build());
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ final class DuelsCountdown {
|
||||||
void start() {
|
void start() {
|
||||||
cancel();
|
cancel();
|
||||||
final int[] counter = {COUNTDOWN_SECONDS};
|
final int[] counter = {COUNTDOWN_SECONDS};
|
||||||
countdownTask = Bukkit.getScheduler().runTaskTimer(LoParkour.getPlugin(),
|
countdownTask = Scheduler.get(LoParkour.getPlugin()).runTimer(
|
||||||
() -> tick(counter), 0L, COUNTDOWN_PERIOD);
|
() -> tick(counter), 0L, COUNTDOWN_PERIOD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,15 +66,13 @@ final class DuelsCountdown {
|
||||||
void tick(@NotNull int[] counter) {
|
void tick(@NotNull int[] counter) {
|
||||||
if (generator.state.stopped()) {
|
if (generator.state.stopped()) {
|
||||||
cancel();
|
cancel();
|
||||||
return;
|
} else {
|
||||||
}
|
final int current = counter[0]--;
|
||||||
final int current = counter[0]--;
|
if (current == 0) {
|
||||||
if (current == 0) {
|
startRace();
|
||||||
startRace();
|
} else if (current <= COUNTDOWN_COLOR_START) {
|
||||||
return;
|
sendCountdownTitle(current);
|
||||||
}
|
}
|
||||||
if (current <= COUNTDOWN_COLOR_START) {
|
|
||||||
sendCountdownTitle(current);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.Mode;
|
||||||
import dev.loki.loparkour.mode.base.Modes;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.scheduler.Scheduler;
|
||||||
import dev.loki.loparkour.session.core.Session;
|
import dev.loki.loparkour.session.core.Session;
|
||||||
import dev.loki.loparkour.util.text.ColorUtil;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
|
@ -87,46 +88,45 @@ public class DuelsGenerator extends ParkourGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addPlayer(@NotNull ParkourPlayer pp) {
|
public void addPlayer(@NotNull ParkourPlayer pp) {
|
||||||
if (!allowJoining || playerGenerators.containsKey(pp)) {
|
if (allowJoining && !playerGenerators.containsKey(pp)) {
|
||||||
return;
|
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);
|
private void placePlayer(@NotNull ParkourPlayer pp, @NotNull SingleDuelsGenerator gen,
|
||||||
gen.setPlayerIndex(playerGenerators.size());
|
@Nullable Location spawnBase) {
|
||||||
gen.state.setHeading(OptionGeneral.heading().getDirection());
|
if (spawnBase != null && spawnBase.getWorld() != null) {
|
||||||
final Location spawnBase = session.getSpawnLocation();
|
Location platformCenter = spawnBase;
|
||||||
if (spawnBase == null || spawnBase.getWorld() == null) {
|
if (pp != owner) {
|
||||||
return;
|
platformCenter = spawnBase.clone().add(0, 0, islandDistance * (double) playerGenerators.size());
|
||||||
}
|
for (int dx = -PLATFORM_RADIUS; dx <= PLATFORM_RADIUS; dx++) {
|
||||||
Location platformCenter = spawnBase;
|
for (int dz = -PLATFORM_RADIUS; dz <= PLATFORM_RADIUS; dz++) {
|
||||||
if (pp != owner) {
|
platformCenter.getBlock().getRelative(dx, 0, dz).setType(Material.STONE);
|
||||||
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) {
|
public void removePlayer(@NotNull ParkourPlayer pp) {
|
||||||
final SingleDuelsGenerator gen = playerGenerators.remove(pp);
|
final SingleDuelsGenerator gen = playerGenerators.remove(pp);
|
||||||
if (gen == null) {
|
if (gen != null) {
|
||||||
return;
|
gen.reset(false);
|
||||||
}
|
if (pp == owner) {
|
||||||
gen.reset(false);
|
countdown.cancel();
|
||||||
if (pp == owner) {
|
returnToLobby();
|
||||||
countdown.cancel();
|
} else if (playerGenerators.size() == 1) {
|
||||||
returnToLobby();
|
win(new ArrayList<>(playerGenerators.keySet()).get(0));
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
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.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);
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ public class CloseMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof CloseMode) {
|
if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof CloseMode)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(CloseGenerator::new, null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
|
|
||||||
Session.create(CloseGenerator::new, null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,21 @@ final class ElytraBoost {
|
||||||
private int boostedSectionIdx = Integer.MIN_VALUE;
|
private int boostedSectionIdx = Integer.MIN_VALUE;
|
||||||
|
|
||||||
void tick(Player player, ElytraSection section, int idx) {
|
void tick(Player player, ElytraSection section, int idx) {
|
||||||
if (!section.isAscending()) {
|
if (section.isAscending()) {
|
||||||
return;
|
boostIfValid(player, section, idx);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void boostIfValid(Player player, ElytraSection section, int idx) {
|
||||||
final Vector pos = player.getLocation().toVector();
|
final Vector pos = player.getLocation().toVector();
|
||||||
final Vector direction = section.end().clone().subtract(section.beginning()).normalize();
|
final Vector direction = section.end().clone().subtract(section.beginning()).normalize();
|
||||||
if (direction.lengthSquared() < EPSILON) {
|
if (direction.lengthSquared() >= EPSILON) {
|
||||||
return;
|
if (idx != boostedSectionIdx && section.isNearKnot(pos, 0, NEAR_KNOT_DISTANCE)) {
|
||||||
}
|
applyInitialBoost(player, idx, direction);
|
||||||
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 (pos.getX() >= section.beginning().getX() && pos.getX() < section.end().getX()) {
|
}
|
||||||
sustain(player, direction);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import org.bukkit.Material;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
@ -93,25 +94,27 @@ public class ElytraGenerator extends ParkourGenerator {
|
||||||
@Override
|
@Override
|
||||||
public void tick() {
|
public void tick() {
|
||||||
final Player player = getPlayer();
|
final Player player = getPlayer();
|
||||||
if (player == null || sections.isEmpty()) {
|
if (player != null && !sections.isEmpty()) {
|
||||||
return;
|
tickPlayer(player);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickPlayer(@NotNull Player player) {
|
||||||
final int idx = sections.keySet().stream().max(Integer::compareTo).orElse(-1);
|
final int idx = sections.keySet().stream().max(Integer::compareTo).orElse(-1);
|
||||||
final ElytraSection section = sections.get(idx);
|
final ElytraSection section = sections.get(idx);
|
||||||
if (section == null) {
|
if (section != null) {
|
||||||
return;
|
final Vector pos = player.getLocation().toVector();
|
||||||
}
|
applyScoring(player, pos);
|
||||||
final Vector pos = player.getLocation().toVector();
|
blockChanger.check(player, material);
|
||||||
applyScoring(player, pos);
|
removeOldest(player);
|
||||||
blockChanger.check(player, material);
|
if (shouldReset(player, pos)) {
|
||||||
removeOldest(player);
|
reset(true);
|
||||||
if (shouldReset(player, pos)) {
|
} else {
|
||||||
reset(true);
|
boost.tick(player, section, idx);
|
||||||
return;
|
if (section.isNearKnot(pos, 2, GENERATE_DISTANCE)) {
|
||||||
}
|
generate();
|
||||||
boost.tick(player, section, idx);
|
}
|
||||||
if (section.isNearKnot(pos, 2, GENERATE_DISTANCE)) {
|
}
|
||||||
generate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ public class ElytraMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ElytraMode) {
|
if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ElytraMode)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(ElytraGenerator::new, null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
|
|
||||||
Session.create(ElytraGenerator::new, null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ public class MinSpeedMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof MinSpeedMode) {
|
if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof MinSpeedMode)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(MinSpeedGenerator::new, null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
|
|
||||||
Session.create(MinSpeedGenerator::new, null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,11 @@ public class ObstacleGenerator extends ElytraGenerator {
|
||||||
private void generateObstacle(int idx, ElytraSection section, int knotIdx) {
|
private void generateObstacle(int idx, ElytraSection section, int knotIdx) {
|
||||||
final Obstacle obstacle = Obstacle.values()[ThreadLocalRandom.current().nextInt(Obstacle.values().length)];
|
final Obstacle obstacle = Obstacle.values()[ThreadLocalRandom.current().nextInt(Obstacle.values().length)];
|
||||||
final List<Vector> points = obstacle.getPoints(section.getKnot(knotIdx), radius());
|
final List<Vector> points = obstacle.getPoints(section.getKnot(knotIdx), radius());
|
||||||
if (points.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Player player = getPlayer();
|
final Player player = getPlayer();
|
||||||
if (player == null) {
|
if (!points.isEmpty() && player != null) {
|
||||||
return;
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ public class ObstacleMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ObstacleMode) {
|
if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof ObstacleMode)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(ObstacleGenerator::new, null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
|
|
||||||
Session.create(ObstacleGenerator::new, null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ public class SpeedDemonMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof SpeedDemonMode) {
|
if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof SpeedDemonMode)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(SpeedDemonGenerator::new, null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
|
|
||||||
Session.create(SpeedDemonGenerator::new, null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ public class TimeTrialMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof TimeTrialMode) {
|
if (!(pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof TimeTrialMode)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(TimeTrialGenerator::new, null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
|
|
||||||
Session.create(TimeTrialGenerator::new, null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,8 @@ public class GeneratorProfileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeMap(generator.state.specialChances);
|
normalizeMap(generator.state.specialChances);
|
||||||
} catch (Exception ignored) {
|
} catch (IllegalArgumentException ignored) {
|
||||||
// special blocks config optional
|
// special blocks config optional: bad numbers/materials only disable specials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ public class JumpValidator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} 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
|
return 0.0; // No adjustment for full blocks
|
||||||
}
|
}
|
||||||
|
|
@ -82,44 +82,31 @@ public class JumpValidator {
|
||||||
final double dy = to.getY() - from.getY();
|
final double dy = to.getY() - from.getY();
|
||||||
final double dz = to.getZ() - from.getZ();
|
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);
|
final double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
|
||||||
if (horizontalDistance < SAME_LOCATION_EPSILON && Math.abs(dy) < SAME_LOCATION_EPSILON) {
|
final boolean sameLocation = horizontalDistance < SAME_LOCATION_EPSILON && Math.abs(dy) < SAME_LOCATION_EPSILON;
|
||||||
return false; // Same location
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check horizontal distance limit
|
|
||||||
if (horizontalDistance > maxHorizontal) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust vertical limits based on slab height
|
// Adjust vertical limits based on slab height
|
||||||
double adjustedMaxVerticalUp = maxVerticalUp;
|
final double adjustedMaxVerticalUp = heightAdjustment < 0
|
||||||
double adjustedMaxVerticalDown = maxVerticalDown;
|
? maxVerticalUp + heightAdjustment
|
||||||
|
: maxVerticalUp;
|
||||||
|
final double adjustedMaxVerticalDown = heightAdjustment < 0
|
||||||
|
? maxVerticalDown - heightAdjustment
|
||||||
|
: maxVerticalDown;
|
||||||
|
|
||||||
if (heightAdjustment < 0) {
|
// For upward jumps, reduce max horizontal distance (player can't jump as far when jumping up)
|
||||||
// Player is lower (bottom slab), reduce upward jump capability
|
final double effectiveMaxHorizontal = dy > 0
|
||||||
adjustedMaxVerticalUp = maxVerticalUp + heightAdjustment; // Reduce max up jump
|
? maxHorizontal - (dy * VERTICAL_REDUCTION_FACTOR)
|
||||||
adjustedMaxVerticalDown = maxVerticalDown - heightAdjustment; // Increase max down jump
|
: maxHorizontal;
|
||||||
}
|
|
||||||
|
|
||||||
// Check vertical limits with adjustment
|
final boolean verticalOk = dy <= adjustedMaxVerticalUp && dy >= -adjustedMaxVerticalDown;
|
||||||
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 double totalDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
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
|
// Check if jump is possible: sqrt(dx² + dy² + dz²) <= maxDistance
|
||||||
|
|
|
||||||
|
|
@ -95,32 +95,27 @@ public class BlockPlacer {
|
||||||
|
|
||||||
final boolean tall = targetSelector.isTallMaterial(blockData.getMaterial());
|
final boolean tall = targetSelector.isTallMaterial(blockData.getMaterial());
|
||||||
final List<Block> blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks();
|
final List<Block> blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks();
|
||||||
if (blocks.isEmpty()) {
|
if (!blocks.isEmpty()) {
|
||||||
return;
|
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() {
|
private void placeSpecialBlock() {
|
||||||
final BlockData specialBlockData = Probs.random(generator.state.specialChances);
|
final BlockData specialBlockData = Probs.random(generator.state.specialChances);
|
||||||
if (specialBlockData == null) {
|
if (specialBlockData == null) {
|
||||||
placeNormalBlock();
|
placeNormalBlock();
|
||||||
return;
|
} else {
|
||||||
|
final boolean tall = targetSelector.isTallMaterial(specialBlockData.getMaterial());
|
||||||
|
final List<Block> 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<Block> 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,18 +88,15 @@ public class LifecycleEventManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyOneTimeRewards(@NotNull ParkourPlayer player, @NotNull Mode mode, int score) {
|
private void applyOneTimeRewards(@NotNull ParkourPlayer player, @NotNull Mode mode, int score) {
|
||||||
if (!Rewards.oneTimeRewards().containsKey(score)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String rewardKey = String.valueOf(score);
|
final String rewardKey = String.valueOf(score);
|
||||||
if (player.settings().collectedRewards().contains(rewardKey)) {
|
if (Rewards.oneTimeRewards().containsKey(score)
|
||||||
return;
|
&& !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() {
|
private void processFall() {
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,13 @@ public class GeneratorCleanup {
|
||||||
}
|
}
|
||||||
|
|
||||||
final int minStandingIndex = findMinStandingIndex();
|
final int minStandingIndex = findMinStandingIndex();
|
||||||
if (minStandingIndex == Integer.MAX_VALUE) {
|
if (minStandingIndex != Integer.MAX_VALUE) {
|
||||||
return;
|
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() {
|
private int findMinStandingIndex() {
|
||||||
|
|
|
||||||
|
|
@ -105,12 +105,11 @@ public class GhostManager {
|
||||||
final File file = new File(playerFolder, data.getTimestamp() + FILE_EXTENSION);
|
final File file = new File(playerFolder, data.getTimestamp() + FILE_EXTENSION);
|
||||||
try {
|
try {
|
||||||
data.saveToFile(file);
|
data.saveToFile(file);
|
||||||
|
evictOldRuns(playerFolder);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LoParkour.getPlugin().getLogger().severe(
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
"Failed to save ghost run for " + data.getPlayerName() + ": " + e.getMessage());
|
"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. */
|
/** Max runs kept per player; read from config each call so reloads apply. */
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,11 @@ public class GhostRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recordFrame(@NotNull Location currentLocation) {
|
public void recordFrame(@NotNull Location currentLocation) {
|
||||||
if (!recording) {
|
if (recording && lastLocation.distance(currentLocation) >= MIN_MOVEMENT) {
|
||||||
return;
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -57,24 +57,24 @@ public class GhostReplay {
|
||||||
public void start() {
|
public void start() {
|
||||||
if (run.getFrames().isEmpty()) {
|
if (run.getFrames().isEmpty()) {
|
||||||
onFinish.run();
|
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() {
|
private void tick() {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public final class HoloHook {
|
||||||
public static void init() {
|
public static void init() {
|
||||||
try {
|
try {
|
||||||
Class.forName("me.filoghost.holographicdisplays.api.HolographicDisplaysAPI");
|
Class.forName("me.filoghost.holographicdisplays.api.HolographicDisplaysAPI");
|
||||||
} catch (Exception ex) {
|
} catch (ClassNotFoundException | LinkageError ex) {
|
||||||
LoParkour.getPlugin().getLogger().warning("##");
|
LoParkour.getPlugin().getLogger().warning("##");
|
||||||
LoParkour.getPlugin().getLogger().warning("## LoParkour only supports Holographic Displays v3.0.0 or higher!");
|
LoParkour.getPlugin().getLogger().warning("## LoParkour only supports Holographic Displays v3.0.0 or higher!");
|
||||||
LoParkour.getPlugin().getLogger().warning("## This hook will now be disabled.");
|
LoParkour.getPlugin().getLogger().warning("## This hook will now be disabled.");
|
||||||
|
|
@ -30,50 +30,47 @@ public final class HoloHook {
|
||||||
return;
|
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) {
|
/** Resolves the {@code {ip_leaderboard: mode, type, #rank}} global placeholder. */
|
||||||
return "?";
|
private static String resolveLeaderboard(String argument) {
|
||||||
}
|
if (argument == null) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
// {ip_leaderboard: default, score, #1}
|
// {ip_leaderboard: default, score, #1}
|
||||||
final String[] split = argument.replace(" ", "").split(",");
|
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 switch (type) {
|
||||||
return "?";
|
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();
|
private static Score lookupScore(Leaderboard leaderboard, String rank) {
|
||||||
|
try {
|
||||||
if (leaderboard == null) {
|
return leaderboard.getScoreAtRank(Integer.parseInt(rank));
|
||||||
return "?";
|
} catch (NumberFormatException e) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
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 String parseDifficulty(String string) {
|
private static String parseDifficulty(String string) {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,23 @@ import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.leaderboard.model.Score;
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
import dev.loki.loparkour.mode.base.Modes;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class GlobalPlaceholderResolver {
|
public class GlobalPlaceholderResolver {
|
||||||
|
|
||||||
private final ScoreRankResolver rankResolver = new ScoreRankResolver();
|
private final ScoreRankResolver rankResolver = new ScoreRankResolver();
|
||||||
private final PlaceholderFormatters formatters = new PlaceholderFormatters();
|
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<RankedMapping> 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) {
|
public String resolve(String params) {
|
||||||
final String value = resolveStatic(params);
|
final String value = resolveStatic(params);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
|
@ -37,22 +49,14 @@ public class GlobalPlaceholderResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveRanked(String params) {
|
private String resolveRanked(String params) {
|
||||||
if (params.contains("player_rank_")) {
|
for (final RankedMapping mapping : rankedMappings) {
|
||||||
return rankResolver.resolve(params.replace("player_rank_", ""), Score::name);
|
if (params.contains(mapping.token())) {
|
||||||
}
|
return rankResolver.resolve(params.replace(mapping.token(), ""), mapping.extractor());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RankedMapping(String token, Function<Score, ?> extractor) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,29 @@ import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class PlayerPlaceholderResolver {
|
public class PlayerPlaceholderResolver {
|
||||||
|
|
||||||
private final PlaceholderFormatters formatters = new PlaceholderFormatters();
|
private final PlaceholderFormatters formatters = new PlaceholderFormatters();
|
||||||
|
|
||||||
|
/** In-session placeholders resolved straight from the player's generator/settings. */
|
||||||
|
private final Map<String, Function<ParkourPlayer, String>> 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) {
|
public String resolve(Player player, String params) {
|
||||||
final String value = resolveLeaderboard(player, params);
|
final String value = resolveLeaderboard(player, params);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
|
@ -55,35 +74,19 @@ public class PlayerPlaceholderResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveGenerator(ParkourPlayer pp, String params) {
|
private String resolveGenerator(ParkourPlayer pp, String params) {
|
||||||
switch (params) {
|
final Function<ParkourPlayer, String> handler = generatorPlaceholders.get(params);
|
||||||
case "score", "current_score" -> {
|
if (handler != null) {
|
||||||
return Integer.toString(pp.session.generator().state.score());
|
return handler.apply(pp);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
private String resolveScoreUntil(ParkourPlayer pp, String params) {
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,20 @@ public final class VaultHook {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (economy == null) {
|
if (economy == null) {
|
||||||
final RegisteredServiceProvider<Economy> service = Bukkit.getServicesManager().getRegistration(Economy.class);
|
economy = resolveEconomy();
|
||||||
|
|
||||||
if (service == null) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Error while trying to fetch the Vault economy - No economy found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
economy = service.getProvider();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
economy.depositPlayer(player, amount);
|
if (economy != null) {
|
||||||
|
economy.depositPlayer(player, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Economy resolveEconomy() {
|
||||||
|
final RegisteredServiceProvider<Economy> 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.core.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.session.core.Session;
|
|
||||||
import dev.loki.loparkour.player.service.UserRegistry;
|
import dev.loki.loparkour.player.service.UserRegistry;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Cancellable;
|
import org.bukkit.event.Cancellable;
|
||||||
|
|
@ -27,53 +26,38 @@ import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.PlayerInventory;
|
import org.bukkit.inventory.PlayerInventory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** Hotbar interactions, chat routing, focus mode and general restrictions for parkour players. */
|
/** Hotbar interactions, chat routing, focus mode and general restrictions for parkour players. */
|
||||||
public class ParkourRestrictionListener implements Listener {
|
public class ParkourRestrictionListener implements Listener {
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onChat(AsyncPlayerChatEvent event) {
|
public void onChat(AsyncPlayerChatEvent event) {
|
||||||
if (!OptionGeneral.optionsEnabled().getOrDefault(ParkourOption.CHAT, true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Player player = event.getPlayer();
|
final Player player = event.getPlayer();
|
||||||
final ParkourUser user = ParkourUser.getUser(player);
|
final ParkourUser user = ParkourUser.getUser(player);
|
||||||
if (user == null) {
|
if (OptionGeneral.optionsEnabled().getOrDefault(ParkourOption.CHAT, true)
|
||||||
return;
|
&& user != null && !user.session.isMuted(user)) {
|
||||||
}
|
event.setCancelled(true);
|
||||||
|
switch (user.chatType()) {
|
||||||
final Session session = user.session;
|
case LOBBY_ONLY -> user.session.getUsers().forEach(
|
||||||
if (session.isMuted(user)) {
|
u -> u.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage()));
|
||||||
return;
|
case PLAYERS_ONLY -> user.session.getPlayers().forEach(
|
||||||
}
|
u -> u.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage()));
|
||||||
|
default -> event.setCancelled(false);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST)
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
public void onCommand(PlayerCommandPreprocessEvent event) {
|
public void onCommand(PlayerCommandPreprocessEvent event) {
|
||||||
if (!Config.CONFIG.getBoolean("focus-mode.enabled")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final ParkourUser user = ParkourUser.getUser(event.getPlayer());
|
final ParkourUser user = ParkourUser.getUser(event.getPlayer());
|
||||||
if (user == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String cmd = event.getMessage().toLowerCase();
|
final String cmd = event.getMessage().toLowerCase();
|
||||||
if (Config.CONFIG.getStringList("focus-mode.whitelist").stream().anyMatch(cmd::contains)) {
|
if (Config.CONFIG.getBoolean("focus-mode.enabled")
|
||||||
return;
|
&& 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)
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
|
@ -93,59 +77,57 @@ public class ParkourRestrictionListener implements Listener {
|
||||||
&& isContainer && event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND) {
|
&& isContainer && event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
player.closeInventory();
|
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
|
final boolean isRightClick = (event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_AIR
|
||||||
|| event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK)
|
|| event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK)
|
||||||
&& event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND;
|
&& event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND;
|
||||||
if (!isRightClick) {
|
if (isRightClick) {
|
||||||
return;
|
final ItemStack held = heldItem(player);
|
||||||
}
|
event.setCancelled(true);
|
||||||
final ItemStack held = heldItem(player);
|
if (!openHotbarMenu(player, held) && !Config.CONFIG.getBoolean("options.disable-inventory-blocks")) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(false);
|
||||||
|
}
|
||||||
if (!openHotbarMenu(player, held) && !Config.CONFIG.getBoolean("options.disable-inventory-blocks")) {
|
|
||||||
event.setCancelled(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean openHotbarMenu(Player player, ItemStack held) {
|
private boolean openHotbarMenu(Player player, ItemStack held) {
|
||||||
if (matchesLocaleHotbar(player, "play.item", held)) {
|
for (final HotbarAction action : HOTBAR_ACTIONS) {
|
||||||
Menus.PLAY.open(player);
|
if (matchesLocaleHotbar(player, action.localeKey(), held)) {
|
||||||
return true;
|
action.apply(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;
|
|
||||||
}
|
}
|
||||||
return false;
|
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<Player> action) {
|
||||||
|
void apply(Player player) {
|
||||||
|
action.accept(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<HotbarAction> 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)
|
@EventHandler(priority = EventPriority.LOW)
|
||||||
public void onInventoryClick(InventoryClickEvent event) {
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
if (!(event.getWhoClicked() instanceof Player player)) {
|
if (event.getWhoClicked() instanceof Player player) {
|
||||||
return;
|
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
|
@EventHandler
|
||||||
|
|
|
||||||
|
|
@ -34,24 +34,22 @@ public class PlayerConnectionListener implements Listener {
|
||||||
|
|
||||||
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
||||||
Modes.defaultMode().create(player);
|
Modes.defaultMode().create(player);
|
||||||
return;
|
} else if (player.getWorld().equals(World.getWorld())) {
|
||||||
}
|
teleportOutOfParkourWorld(player);
|
||||||
|
|
||||||
if (!player.getWorld().equals(World.getWorld())) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void teleportOutOfParkourWorld(Player player) {
|
||||||
final org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back"));
|
final org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back"));
|
||||||
if (fallback != null) {
|
if (fallback != null) {
|
||||||
PaperLib.teleportAsync(player, fallback.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
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
|
@EventHandler
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,11 @@ public class SchematicWandListener implements Listener {
|
||||||
|
|
||||||
@EventHandler(ignoreCancelled = true)
|
@EventHandler(ignoreCancelled = true)
|
||||||
public void onInteract(@NotNull PlayerInteractEvent event) {
|
public void onInteract(@NotNull PlayerInteractEvent event) {
|
||||||
if (event.getHand() != EquipmentSlot.HAND) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!SchematicCommandHandler.isWand(event.getItem())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Action action = event.getAction();
|
final Action action = event.getAction();
|
||||||
if (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) {
|
if (event.getHand() != EquipmentSlot.HAND
|
||||||
return;
|
|| !SchematicCommandHandler.isWand(event.getItem())
|
||||||
}
|
|| (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK)
|
||||||
if (event.getClickedBlock() == null) {
|
|| event.getClickedBlock() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,17 +107,16 @@ public class SingleLeaderboardMenu extends LPMenu {
|
||||||
|
|
||||||
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull,
|
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull,
|
||||||
@NotNull UUID id, int rank) {
|
@NotNull UUID id, int rank) {
|
||||||
if (rank > SKULL_RANK_LIMIT || ParkourUser.isBedrockPlayer(player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final OfflinePlayer op = org.bukkit.Bukkit.getOfflinePlayer(id);
|
final OfflinePlayer op = org.bukkit.Bukkit.getOfflinePlayer(id);
|
||||||
if (op.getName() == null || op.getName().startsWith(".")) {
|
if (rank <= SKULL_RANK_LIMIT
|
||||||
return;
|
&& !ParkourUser.isBedrockPlayer(player)
|
||||||
}
|
&& op.getName() != null
|
||||||
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
&& !op.getName().startsWith(".")) {
|
||||||
if (meta != null) {
|
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||||
meta.setOwningPlayer(op);
|
if (meta != null) {
|
||||||
skull.setItemMeta(meta);
|
meta.setOwningPlayer(op);
|
||||||
|
skull.setItemMeta(meta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,26 +39,22 @@ public final class GuiManager implements Listener {
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onInventoryClick(InventoryClickEvent event) {
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
if (!(event.getWhoClicked() instanceof Player player)) {
|
if (event.getWhoClicked() instanceof Player player) {
|
||||||
return;
|
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
|
@EventHandler
|
||||||
public void onInventoryClose(InventoryCloseEvent event) {
|
public void onInventoryClose(InventoryCloseEvent event) {
|
||||||
if (!(event.getPlayer() instanceof Player player)) {
|
if (event.getPlayer() instanceof Player player) {
|
||||||
return;
|
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
|
@EventHandler
|
||||||
|
|
|
||||||
|
|
@ -108,16 +108,13 @@ public class DuelsMenu extends LPMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser owner) {
|
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser owner) {
|
||||||
if (ParkourUser.isBedrockPlayer(player)) {
|
final String name = owner.getName();
|
||||||
return;
|
if (!ParkourUser.isBedrockPlayer(player) && name != null && !name.startsWith(".")) {
|
||||||
}
|
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||||
if (owner.getName() == null || owner.getName().startsWith(".")) {
|
if (meta != null) {
|
||||||
return;
|
meta.setOwningPlayer(owner.player);
|
||||||
}
|
skull.setItemMeta(meta);
|
||||||
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
}
|
||||||
if (meta != null) {
|
|
||||||
meta.setOwningPlayer(owner.player);
|
|
||||||
skull.setItemMeta(meta);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,16 +84,13 @@ public class SpectatorMenu extends LPMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser pp) {
|
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser pp) {
|
||||||
if (ParkourUser.isBedrockPlayer(player)) {
|
final String name = pp.getName();
|
||||||
return;
|
if (!ParkourUser.isBedrockPlayer(player) && name != null && !name.startsWith(".")) {
|
||||||
}
|
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||||
if (pp.getName() == null || pp.getName().startsWith(".")) {
|
if (meta != null) {
|
||||||
return;
|
meta.setOwningPlayer(pp.player);
|
||||||
}
|
skull.setItemMeta(meta);
|
||||||
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
}
|
||||||
if (meta != null) {
|
|
||||||
meta.setOwningPlayer(pp.player);
|
|
||||||
skull.setItemMeta(meta);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,9 @@ public class InvisibleBarrierMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() instanceof BarrierGenerator) {
|
if (!(pp != null && pp.session.generator() instanceof BarrierGenerator)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(session -> new BarrierGenerator(session), null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
Session.create(session -> new BarrierGenerator(session), null, null, player);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,37 +53,29 @@ public class CoopMode implements MultiMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() instanceof CoopGenerator) {
|
if (!(pp != null && pp.session.generator() instanceof CoopGenerator)) {
|
||||||
return;
|
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
|
@Override
|
||||||
public void join(Player joiningPlayer, Session session) {
|
public void join(Player joiningPlayer, Session session) {
|
||||||
if (!ModeMessages.checkJoiningEnabled(joiningPlayer)) {
|
if (ModeMessages.checkJoiningEnabled(joiningPlayer) && session.generator() != null) {
|
||||||
return;
|
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) {
|
session.getPlayers().forEach(p ->
|
||||||
return;
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -66,12 +66,10 @@ public class RaceMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() instanceof RaceGenerator) {
|
if (!(pp != null && pp.session.generator() instanceof RaceGenerator)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(session -> new RaceGenerator(session), null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
Session.create(session -> new RaceGenerator(session), null, null, player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── RaceGenerator ─────────────────────────────────────────────────────────
|
// ── RaceGenerator ─────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -45,23 +45,18 @@ public class SpectatorMode implements Mode {
|
||||||
}
|
}
|
||||||
if (!ParkourOption.SPECTATOR.mayPerform(player)) {
|
if (!ParkourOption.SPECTATOR.mayPerform(player)) {
|
||||||
player.sendMessage(Locales.getString(player, "other.no_do"));
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,11 @@ public class DefaultMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() != null && pp.session.generator().getMode() instanceof DefaultMode) {
|
final boolean alreadyPlaying = pp != null && pp.session.generator() != null
|
||||||
return;
|
&& 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,10 @@ public class GravityShiftMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() instanceof GravityShiftGenerator) {
|
if (!(pp != null && pp.session.generator() instanceof GravityShiftGenerator)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(session -> new GravityShiftGenerator(session), null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
Session.create(session -> new GravityShiftGenerator(session), null, null, player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GravityShiftGenerator ─────────────────────────────────────────────────
|
// ── GravityShiftGenerator ─────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,10 @@ public class SpeedrunMode implements Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
final ParkourPlayer pp = UserRegistry.getPlayer(player);
|
||||||
if (pp != null && pp.session.generator() instanceof SpeedrunGenerator) {
|
if (!(pp != null && pp.session.generator() instanceof SpeedrunGenerator)) {
|
||||||
return;
|
player.closeInventory();
|
||||||
|
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.closeInventory();
|
|
||||||
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SpeedrunGenerator ─────────────────────────────────────────────────────
|
// ── SpeedrunGenerator ─────────────────────────────────────────────────────
|
||||||
|
|
@ -106,19 +104,12 @@ public class SpeedrunMode implements Mode {
|
||||||
// The block the player just landed on is the previous-to-last in history
|
// 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)
|
// (history.size()-1 is the leading block, size()-2 is where player stands)
|
||||||
final int histSize = state.history.size();
|
final int histSize = state.history.size();
|
||||||
if (histSize < 2) {
|
if (histSize >= 2) {
|
||||||
return;
|
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) {
|
private void scheduleBlockRemoval(@NotNull Block block) {
|
||||||
|
|
|
||||||
|
|
@ -57,24 +57,30 @@ public class InventoryData {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFile(Consumer<@Nullable InventoryData> onFinish) {
|
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 {
|
try {
|
||||||
final YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
|
final YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
|
||||||
if (!yaml.getKeys(false).isEmpty()) {
|
if (!yaml.getKeys(false).isEmpty()) {
|
||||||
loadFromYaml(yaml);
|
loadFromYaml(yaml);
|
||||||
onFinish.accept(this);
|
onFinish.accept(this);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
// Boundary: corrupt player data falls back to legacy format, never breaks join.
|
||||||
LoParkour.getPlugin().getLogger().warning(
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
"YAML inventory parse failed for %s, trying legacy format: %s".formatted(file.getName(), ex.getMessage()));
|
"YAML inventory parse failed for %s, trying legacy format: %s".formatted(file.getName(), ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
if (file.length() == 0) {
|
|
||||||
onFinish.accept(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadLegacy(onFinish);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFromYaml(YamlConfiguration yaml) {
|
private void loadFromYaml(YamlConfiguration yaml) {
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ public class PreviousData {
|
||||||
player.resetPlayerWeather();
|
player.resetPlayerWeather();
|
||||||
player.setVelocity(new Vector(0, 0, 0));
|
player.setVelocity(new Vector(0, 0, 0));
|
||||||
player.setFallDistance(0f);
|
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());
|
LoParkour.getPlugin().getLogger().severe("Error while recovering stats of %s".formatted(player.getName()) + " - " + ex.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,15 +75,13 @@ public final class UserRegistry {
|
||||||
user.unregister();
|
user.unregister();
|
||||||
resetScoreboard(user);
|
resetScoreboard(user);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
// Boundary: listener/scoreboard failures must not break the leave path.
|
||||||
handleUnregisterError(user, ex);
|
handleUnregisterError(user, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restorePreviousData && shouldKickToBungee(kickIfBungee)) {
|
if (restorePreviousData && shouldKickToBungee(kickIfBungee)) {
|
||||||
BungeeUtil.sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
|
BungeeUtil.sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
|
||||||
return;
|
} else if (restorePreviousData) {
|
||||||
}
|
|
||||||
|
|
||||||
if (restorePreviousData) {
|
|
||||||
restorePreviousState(user, urgent);
|
restorePreviousState(user, urgent);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ public class ScoreboardManager {
|
||||||
try {
|
try {
|
||||||
return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, text);
|
return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, text);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
// Boundary: external PlaceholderAPI may throw on version skew; scoreboard must survive.
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,23 +81,17 @@ public class ParkourSpectator extends ParkourUser {
|
||||||
player.setGameMode(GameMode.SPECTATOR);
|
player.setGameMode(GameMode.SPECTATOR);
|
||||||
updateScoreboard(session.generator());
|
updateScoreboard(session.generator());
|
||||||
|
|
||||||
// spectator is still being teleported to world
|
// spectator is still being teleported to world (same world) and far enough from the followed player
|
||||||
if (closest.getLocation().getWorld() != player.getLocation().getWorld()) {
|
final boolean sameWorld = closest.getLocation().getWorld() == player.getLocation().getWorld();
|
||||||
return;
|
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -26,41 +26,34 @@ public record Reward(@NotNull String string) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String command = this.string;
|
|
||||||
|
|
||||||
final Mode rewardMode = Registry.getModes().stream()
|
final Mode rewardMode = Registry.getModes().stream()
|
||||||
.filter(m -> this.string.contains("%s:".formatted(m.getName().toLowerCase())))
|
.filter(m -> this.string.contains("%s:".formatted(m.getName().toLowerCase())))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(Modes.defaultMode());
|
.orElse(Modes.defaultMode());
|
||||||
|
|
||||||
if (mode != rewardMode) {
|
if (mode == rewardMode) {
|
||||||
return;
|
String command = string.replaceFirst("%s:".formatted(rewardMode.getName().toLowerCase()), "");
|
||||||
}
|
if (command.toLowerCase().contains("%player%")) {
|
||||||
|
command = command.replaceAll("%player%", player.getName());
|
||||||
command = command.replaceFirst("%s:".formatted(rewardMode.getName().toLowerCase()), "");
|
}
|
||||||
|
applyCommand(player, command);
|
||||||
// check for placeholders
|
|
||||||
if (command.toLowerCase().contains("%player%")) {
|
|
||||||
command = command.replaceAll("%player%", player.getName());
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCommand(@NotNull ParkourPlayer player, String command) {
|
||||||
// check for extra data
|
// check for extra data
|
||||||
if (command.toLowerCase().contains("leave:")) { // leave:
|
if (command.toLowerCase().contains("leave:")) { // leave:
|
||||||
command = command.replaceFirst("leave:", "");
|
player.previousData().onLeave.add(new Reward(command.replaceFirst("leave:", "")));
|
||||||
player.previousData().onLeave.add(new Reward(command));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for things to perform
|
// check for things to perform
|
||||||
if (command.toLowerCase().contains("send:")) {
|
if (command.toLowerCase().contains("send:")) {
|
||||||
command = command.replaceFirst("send:", "");
|
player.send(command.replaceFirst("send:", ""));
|
||||||
|
|
||||||
player.send(command);
|
|
||||||
} else if (command.toLowerCase().contains("vault:")) {
|
} else if (command.toLowerCase().contains("vault:")) {
|
||||||
command = command.replaceFirst("vault:", "");
|
final String amount = command.replaceFirst("vault:", "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
VaultHook.deposit(player.player, Double.parseDouble(command));
|
VaultHook.deposit(player.player, Double.parseDouble(amount));
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
LoParkour.getPlugin().getLogger().severe(
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
"Error while trying to process Vault reward - check your rewards file for incorrect numbers - "
|
"Error while trying to process Vault reward - check your rewards file for incorrect numbers - "
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,9 @@ final class BukkitBackend implements Scheduler.Backend {
|
||||||
public ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) {
|
public ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) {
|
||||||
return Bukkit.getScheduler().runTaskTimer(plugin, task, delayTicks, periodTicks)::cancel;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ final class FoliaBackend implements Scheduler.Backend {
|
||||||
private final Method globalRunDelayed;
|
private final Method globalRunDelayed;
|
||||||
private final Method globalRunAtFixedRate;
|
private final Method globalRunAtFixedRate;
|
||||||
private final Method asyncRunNow;
|
private final Method asyncRunNow;
|
||||||
|
private final Method asyncRunAtFixedRate;
|
||||||
private final Method taskCancel;
|
private final Method taskCancel;
|
||||||
|
|
||||||
FoliaBackend(Plugin plugin) throws ReflectiveOperationException {
|
FoliaBackend(Plugin plugin) throws ReflectiveOperationException {
|
||||||
|
|
@ -40,6 +41,8 @@ final class FoliaBackend implements Scheduler.Backend {
|
||||||
this.globalRunAtFixedRate = method(GLOBAL_SCHEDULER_CLASS, "runAtFixedRate",
|
this.globalRunAtFixedRate = method(GLOBAL_SCHEDULER_CLASS, "runAtFixedRate",
|
||||||
Plugin.class, Consumer.class, long.class, long.class);
|
Plugin.class, Consumer.class, long.class, long.class);
|
||||||
this.asyncRunNow = method(ASYNC_SCHEDULER_CLASS, "runNow", Plugin.class, Consumer.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");
|
this.taskCancel = Class.forName(SCHEDULER_PACKAGE + "ScheduledTask").getMethod("cancel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +67,12 @@ final class FoliaBackend implements Scheduler.Backend {
|
||||||
consumer(task), delayTicks, periodTicks));
|
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 {
|
private static Object scheduler(String bukkitGetter) throws ReflectiveOperationException {
|
||||||
return Bukkit.class.getMethod(bukkitGetter).invoke(null);
|
return Bukkit.class.getMethod(bukkitGetter).invoke(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,18 @@ public final class Scheduler {
|
||||||
return backend.runTimer(task, delayTicks, periodTicks);
|
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) {
|
private static Backend createBackend(Plugin plugin) {
|
||||||
if (FOLIA) {
|
if (FOLIA) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -118,5 +130,7 @@ public final class Scheduler {
|
||||||
ScheduledTask runLater(Runnable task, long delayTicks);
|
ScheduledTask runLater(Runnable task, long delayTicks);
|
||||||
|
|
||||||
ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks);
|
ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks);
|
||||||
|
|
||||||
|
ScheduledTask runTimerAsync(Runnable task, long delayTicks, long periodTicks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ public class SessionStateManager {
|
||||||
if (LoParkour.getGhostService() != null) {
|
if (LoParkour.getGhostService() != null) {
|
||||||
LoParkour.getGhostService().recordPlayers(session.generator());
|
LoParkour.getGhostService().recordPlayers(session.generator());
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
} catch (RuntimeException t) {
|
||||||
session.generator().session.getPlayers().forEach(p ->
|
session.generator().session.getPlayers().forEach(p ->
|
||||||
p.player.sendMessage("§c[LoParkour] Tick error: " + t.getMessage()));
|
p.player.sendMessage("§c[LoParkour] Tick error: " + t.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,11 +102,10 @@ public final class StorageDisk {
|
||||||
LoParkour.getPlugin().getLogger().warning(
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
"Player data file for %s is empty or corrupted, using defaults".formatted(player.getName()));
|
"Player data file for %s is empty or corrupted, using defaults".formatted(player.getName()));
|
||||||
PlayerSettingsManager.applyDefaults(player);
|
PlayerSettingsManager.applyDefaults(player);
|
||||||
return;
|
} else {
|
||||||
|
normalizeLegacyKeys(settings);
|
||||||
|
PlayerSettingsManager.applySettings(player, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeLegacyKeys(settings);
|
|
||||||
PlayerSettingsManager.applySettings(player, settings);
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
LoParkour.getPlugin().getLogger().severe(
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
"Error while trying to read disk data of %s".formatted(player.getName()) + " - " + ex.getMessage());
|
"Error while trying to read disk data of %s".formatted(player.getName()) + " - " + ex.getMessage());
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ public class SQLConnectionManager {
|
||||||
|
|
||||||
executeCallbacks();
|
executeCallbacks();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
// Boundary: pool failure taxonomy is not public API; connect must never break enable.
|
||||||
handleConnectionError(ex);
|
handleConnectionError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +104,7 @@ public class SQLConnectionManager {
|
||||||
try {
|
try {
|
||||||
cb.run();
|
cb.run();
|
||||||
} catch (Exception e) {
|
} 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());
|
LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses {@link Material} names from config (modern IDs, registry, legacy aliases).
|
* Parses {@link Material} names from config (modern IDs, registry, legacy aliases).
|
||||||
|
|
@ -27,46 +28,33 @@ public final class Materials {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final String key = raw.toLowerCase(Locale.ROOT).replace(' ', '_');
|
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(':');
|
final int colon = key.indexOf(':');
|
||||||
if (colon >= 0) {
|
final String colonId = colon >= 0 ? key.substring(colon + 1) : null;
|
||||||
final String id = key.substring(colon + 1);
|
|
||||||
m = Material.matchMaterial(id, false);
|
return firstNonNull(
|
||||||
if (m != null) {
|
() -> Material.matchMaterial(key, false),
|
||||||
return m;
|
() -> fromRegistry(key),
|
||||||
}
|
() -> Material.matchMaterial(raw.toUpperCase(Locale.ROOT), false),
|
||||||
m = fromRegistry(id);
|
() -> 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<Material>... candidates) {
|
||||||
|
for (final Supplier<Material> candidate : candidates) {
|
||||||
|
final Material m = candidate.get();
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,12 @@ public final class World {
|
||||||
|
|
||||||
final var existingWorld = Bukkit.getWorld(name);
|
final var existingWorld = Bukkit.getWorld(name);
|
||||||
|
|
||||||
if (!Config.CONFIG.getBoolean("joining")) {
|
if (Config.CONFIG.getBoolean("joining")) {
|
||||||
return;
|
createOrReuse(existingWorld);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createOrReuse(org.bukkit.World existingWorld) {
|
||||||
if (existingWorld != null) {
|
if (existingWorld != null) {
|
||||||
LoParkour.getPlugin().getLogger().warning(
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
"Crash detected! The parkour world loading twice is not usual behaviour. " +
|
"Crash detected! The parkour world loading twice is not usual behaviour. " +
|
||||||
|
|
@ -134,10 +136,12 @@ public final class World {
|
||||||
LoParkour.log("Deleting Spigot world");
|
LoParkour.log("Deleting Spigot world");
|
||||||
|
|
||||||
final File folder = worldFolder();
|
final File folder = worldFolder();
|
||||||
if (!folder.exists()) {
|
if (folder.exists()) {
|
||||||
return;
|
deleteWorldFolder(folder);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteWorldFolder(File folder) {
|
||||||
try {
|
try {
|
||||||
Bukkit.unloadWorld(name, false);
|
Bukkit.unloadWorld(name, false);
|
||||||
} catch (UnsupportedOperationException ex) {
|
} catch (UnsupportedOperationException ex) {
|
||||||
|
|
|
||||||
|
|
@ -25,28 +25,28 @@ Player setting **0.25 (easy)** → only structures with difficulty ≤ 0.25.
|
||||||
|
|
||||||
## Creating schematics in-game (admin)
|
## Creating schematics in-game (admin)
|
||||||
|
|
||||||
1. Get the wand: `/lp schematic wand`
|
1. Get the wand: `/lpk schematic wand`
|
||||||
2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`)
|
2. **Left-click** = position 1, **right-click** = position 2 (or use `/lpk schematic pos1` / `pos2`)
|
||||||
3. Save and register:
|
3. Save and register:
|
||||||
- `/lp create <difficulty>` — auto name `parkour-<hash>.schem` + YAML entry
|
- `/lpk create <difficulty>` — auto name `parkour-<hash>.schem` + YAML entry
|
||||||
- `/lp schematic create <difficulty>` — same
|
- `/lpk schematic create <difficulty>` — same
|
||||||
- `/lp schematic create <name> <difficulty>` — custom name (letters, numbers, `-`, `_`)
|
- `/lpk schematic create <name> <difficulty>` — custom name (letters, numbers, `-`, `_`)
|
||||||
|
|
||||||
Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1).
|
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
|
## Admin commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/lp schematic wand` | Selection wand |
|
| `/lpk schematic wand` | Selection wand |
|
||||||
| `/lp schematic pos1` / `pos2` | Set corners at your feet |
|
| `/lpk schematic pos1` / `pos2` | Set corners at your feet |
|
||||||
| `/lp create <difficulty>` | Save selection → `.schem` + YAML |
|
| `/lpk create <difficulty>` | Save selection → `.schem` + YAML |
|
||||||
| `/lp schematic create <name> <difficulty>` | Save with custom name |
|
| `/lpk schematic create <name> <difficulty>` | Save with custom name |
|
||||||
| `/lp schematic list` | List loaded structures |
|
| `/lpk schematic list` | List loaded structures |
|
||||||
| `/lp schematic paste <name>` | Test-paste at your location |
|
| `/lpk schematic paste <name>` | Test-paste at your location |
|
||||||
| `/lp schematic reload` | Reload disk files |
|
| `/lpk schematic reload` | Reload disk files |
|
||||||
|
|
||||||
## Flow (runtime)
|
## Flow (runtime)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ main: dev.loki.loparkour.LoParkour
|
||||||
softdepend: [floodgate, Vault, PlaceholderAPI, Multiverse-Core]
|
softdepend: [floodgate, Vault, PlaceholderAPI, Multiverse-Core]
|
||||||
commands:
|
commands:
|
||||||
loparkour:
|
loparkour:
|
||||||
aliases: [parkour, lp]
|
aliases: [parkour, lpk]
|
||||||
usage: /<command> [arguments]
|
usage: /<command> [arguments]
|
||||||
description: Main command for LoParkour
|
description: Main command for LoParkour
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# -------- Structure difficulty --------
|
# -------- Structure difficulty --------
|
||||||
# plugins/LoParkour/schematics/
|
# plugins/LoParkour/schematics/
|
||||||
# .nbt — structure block export
|
# .nbt — structure block export
|
||||||
# .schem / .schematic — WorldEdit or /lp create
|
# .schem / .schematic — WorldEdit or /lpk create
|
||||||
# In-game: /lp schematic wand → select → /lp create <difficulty>
|
# In-game: /lpk schematic wand → select → /lpk create <difficulty>
|
||||||
# Docs: plugins/LoParkour/docs/SCHEMATICS.md
|
# Docs: plugins/LoParkour/docs/SCHEMATICS.md
|
||||||
|
|
||||||
# -------- The difficulty of schematics --------
|
# -------- The difficulty of schematics --------
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
Class <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1> extends class <org.bukkit.scheduler.BukkitRunnable> in (MetricsCacheStore.java:0)
|
|
||||||
Constructor <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1.<init>(dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> calls constructor <org.bukkit.scheduler.BukkitRunnable.<init>()> in (MetricsCacheStore.java:84)
|
|
||||||
Field <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.autoSaveTask> has type <org.bukkit.scheduler.BukkitRunnable> in (MetricsCacheStore.java:0)
|
|
||||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls constructor <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1.<init>(dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> in (MetricsCacheStore.java:84)
|
|
||||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.cancel()> in (MetricsCacheStore.java:81)
|
|
||||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.runTaskTimerAsynchronously(org.bukkit.plugin.Plugin, long, long)> in (MetricsCacheStore.java:91)
|
|
||||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.stopAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.cancel()> in (MetricsCacheStore.java:96)
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
Method <dev.loki.loparkour.duels.DuelsCountdown.giveStartItem(dev.loki.loparkour.player.core.ParkourPlayer)> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsCountdown.java:45)
|
|
||||||
Method <dev.loki.loparkour.duels.DuelsCountdown.start()> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsCountdown.java:55)
|
|
||||||
Method <dev.loki.loparkour.duels.DuelsGenerator.win(dev.loki.loparkour.player.core.ParkourPlayer)> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsGenerator.java:137)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue