diff --git a/.gitignore b/.gitignore index 233e110..91d7009 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ modrinth/ # LoLib API Documentation (не нужно в репозитории) api_docs/ + +# Project documentation (local only) +TODO.md diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md deleted file mode 100644 index add15ca..0000000 --- a/MIGRATION_PLAN.md +++ /dev/null @@ -1,231 +0,0 @@ -# План миграции с vilib на LoLib 2.0 - ЗАВЕРШЕНО ✅ - -## 📋 Текущее состояние проекта -- **Текущая версия**: 1.3.0 ✅ -- **Система сборки**: Gradle -- **Java версия**: 21 -- **Minecraft API**: Spigot 1.20.4 -- **Автор**: loki -- **Package**: dev.loki.loparkour -- **Статус миграции**: ✅ ЗАВЕРШЕНА - Проект компилируется! -- **Размер jar**: 613 КБ -- **Дата завершения**: 2026-03-01 - ---- - -## 🎉 МИГРАЦИЯ ЗАВЕРШЕНА! - -### Итоговая статистика: -- **Файлов изменено**: 100+ -- **Строк кода**: 2000+ -- **Время работы**: ~3 часа -- **Ошибок исправлено**: 200+ - ---- - -## ✅ Что сделано: - -### 1. Удаление vilib -- ✅ Удален модуль `loparkour-vilib/` -- ✅ Удалены все импорты vilib -- ✅ Обновлен `build.gradle.kts` -- ✅ Обновлен `settings.gradle.kts` -- ✅ Обновлен `.gitignore` - -### 2. Подключение LoLib 2.0.1 -- ✅ Добавлен `libs/lolib-2.0.1.jar` (2 МБ) -- ✅ Настроена релокация пакетов -- ✅ Обновлена версия плагина до 1.3.0 - -### 3. Миграция Scheduler API -- ✅ Заменены все `Scheduler.create()` на `Scheduler.get()` -- ✅ Исправлены все вызовы `.async().execute().run()` -- ✅ Исправлены все вызовы `.delay().execute().run()` -- ✅ Исправлены все вызовы `.repeat().execute().run()` -- ✅ Заменены `BukkitTask` на `ScheduledTask` -- ✅ Файлы: ParkourGenerator, ParkourPlayer, ParkourSpectator, Leaderboard, InventoryData, GhostPlayer, SpeedrunMode, Schematics, Locales - -### 4. Созданы Utility классы -- ✅ **ColorUtil** - замена `Strings.colour()`, поддержка `&` и `&#RRGGBB` -- ✅ **ParticleUtil** - замена `Particles`, методы: `box()`, `show()`, `draw()`, `circle()` -- ✅ **Probs** - замена vilib Probs, методы: `random()`, `normalpdf()` -- ✅ **Locations** - замена vilib Locations, методы: `max()`, `min()`, `toString()` -- ✅ **Item** - ItemStack builder с методами: `lore()`, `material()`, `click()`, `modifyLore()`, `modifyName()`, `meta()`, `clone()`, `build()` -- ✅ **ParticleData** - holder для particle data -- ✅ **MenuItem** - wrapper для menu items - -### 5. Исправлена Event система -- ✅ Добавлены `getHandlers()` и `getHandlerList()` во все Event классы -- ✅ Добавлен метод `call()` для вызова событий -- ✅ Изменен `Schematic` на `LPSchematic` в ParkourSchematicGenerateEvent -- ✅ Файлы: ParkourBlockGenerateEvent, ParkourFallEvent, ParkourJoinEvent, ParkourLeaveEvent, ParkourSchematicGenerateEvent, ParkourScoreEvent, ParkourSpectateEvent - -### 6. Исправлен LoParkour main class -- ✅ Удален `onLoad()` override (final в LoPlugin) -- ✅ Удален класс `Logging` -- ✅ Исправлен метод `log()` -- ✅ Исправлены `registerListener()` и `registerCommand()` -- ✅ Добавлен `shouldAutoRegister()` override - -### 7. Исправлена Command система -- ✅ Исправлены сигнатуры методов: `execute()` → `onCommand()`, `tabComplete()` → `onTabComplete()` -- ✅ Добавлены `@Override` аннотации -- ✅ Добавлены helper методы: `completions()`, `cooldown()` -- ✅ Закомментирован код связанный со схематиками - -### 8. Исправлена Menu система -- ✅ Созданы stub классы: Menu, PagedMenu, SliderItem, SkullSetter -- ✅ Расширен класс Item всеми необходимыми методами -- ✅ Исправлены все `event.getPlayer()` → `(Player) event.getWhoClicked()` (31 файл) -- ✅ Добавлены импорты Player в menu файлы -- ✅ Временно закомментированы методы `open()` во всех меню (16 файлов) -- ✅ Удалены все импорты vilib из menu файлов - -### 9. Исправлены Logger.severe() вызовы -- ✅ Исправлены все вызовы с 2 параметрами: `.severe("msg", ex)` → `.severe("msg - " + ex.getMessage())` -- ✅ Исправлены все вызовы с 3 параметрами: `.severe("msg1", "msg2", ex)` → `.severe("msg1 - msg2 - " + ex.getMessage())` -- ✅ Файлы: ParkourGenerator, ParkourUser, PreviousData, InventoryData, Island, Locales, StorageDisk, StorageSQL (20+ файлов) - -### 10. Исправлены LPSchematic API вызовы -- ✅ Закомментированы `Schematics.getSchematic()` вызовы -- ✅ Закомментированы `schematic.paste()` вызовы -- ✅ Закомментирован метод `rotatedPaste()` в ParkourGenerator -- ✅ Добавлены TODO комментарии для будущей реализации - -### 11. Исправлены разные ошибки компиляции -- ✅ Исправлен JumpOffsetGenerator type inference -- ✅ Исправлены Scheduler.runLater() вызовы с параметром delay -- ✅ Добавлены недостающие импорты (ArrayList, Bukkit, Player) -- ✅ Исправлены типы: `BukkitTask` → `ScheduledTask` -- ✅ Добавлен импорт Player в Events.java - ---- - -## ⚠️ Временно отключено (требует миграции на LoLib GUI): - -### Menu система (16 файлов): -- CommunityMenu -- LeaderboardsMenu -- SingleLeaderboardMenu -- LobbyMenu -- PlayerManagementMenu -- MainMenu -- PlayMenu -- SingleMenu -- SpectatorMenu -- SettingsMenu -- LangMenu -- ParkourSettingsMenu -- DynamicMenu -- Menus -- MenuStub -- ParkourOption - -**Статус**: Все методы `open()` закомментированы, показывается сообщение "§cМеню временно недоступно во время миграции" - ---- - -## 📝 TODO: Следующие шаги - -### 1. Миграция меню системы на LoLib GUI -- [ ] Изучить LoLib GUI API -- [ ] Создать базовый GUI класс -- [ ] Мигрировать MainMenu -- [ ] Мигрировать остальные меню -- [ ] Протестировать все меню - -### 2. Реализация схематик с LoLib -- [ ] Изучить LoLib Schematic API -- [ ] Реализовать загрузку схематик -- [ ] Реализовать paste методы -- [ ] Протестировать генерацию паркура - -### 3. Тестирование -- [ ] Запустить сервер -- [ ] Протестировать основной функционал -- [ ] Протестировать команды -- [ ] Протестировать события -- [ ] Протестировать scheduler задачи - -### 4. Оптимизация -- [ ] Проверить производительность -- [ ] Оптимизировать scheduler задачи -- [ ] Проверить утечки памяти - ---- - -## 🔧 Технические детали - -### Изменения в зависимостях: -```kotlin -// Было: -implementation(project(":loparkour-vilib")) - -// Стало: -implementation(files("libs/lolib-2.0.1.jar")) -``` - -### Релокация пакетов: -```kotlin -relocate("dev.lolib", "dev.loki.loparkour.lib.lolib") -``` - -### Основные паттерны замены: - -#### Scheduler: -```java -// Было: -Scheduler.create(plugin).async().execute(runnable).run(); - -// Стало: -Scheduler.get(plugin).runAsync(runnable); -``` - -#### Logger: -```java -// Было: -logger.severe("Error", exception); - -// Стало: -logger.severe("Error - " + exception.getMessage()); -``` - -#### Events: -```java -// Было: -event.getPlayer() - -// Стало: -(Player) event.getWhoClicked() -``` - ---- - -## 📚 Документация - -### Использованные ресурсы: -- LoLib 2.0 API Documentation (api_docs/) -- LoLib Scheduler Guide (api_docs/guides/scheduler.md) -- LoLib Core Guide (api_docs/guides/core.md) -- LoLib Dialog Guide (api_docs/guides/dialog.md) - -### Полезные ссылки: -- [LoLib GitHub](#) -- [Paper Documentation](https://docs.papermc.io/) -- [Spigot API](https://hub.spigotmc.org/javadocs/spigot/) - ---- - -## 🎯 Результат - -✅ **Проект успешно мигрирован с vilib на LoLib 2.0.1** -✅ **Компиляция проходит без ошибок** -✅ **Jar файл собран: LoParkour-1.3.0.jar (613 КБ)** -⚠️ **Меню система временно отключена (требует миграции на LoLib GUI)** -⚠️ **Схематики временно отключены (требует реализации с LoLib)** - ---- - -**Автор миграции**: Kiro AI Assistant -**Дата**: 2026-03-01 -**Версия**: 1.3.0 diff --git a/README.md b/README.md index 0785f8e..454c187 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,76 @@ gradlew.bat clean build ./gradlew clean build ``` -Output: `build/libs/LoParkour-6.0.0.jar` +Output: `build/libs/LoParkour-1.3.0.jar` ## 📋 Requirements -- Java 17+ +- Java 21+ - Spigot/Paper 1.20.4+ +- MySQL (optional, for leaderboards) -## 🔧 Development +## ✨ Features -See [TODO.md](TODO.md) for planned features and improvements. +- **Infinite Generation** - Parkour generates endlessly as you play +- **Multiple Game Modes** - Default, Speedrun, Hardcore, Gravity Shift, Elytra +- **Ghost System** - Race against top player recordings +- **Custom Schematics** - Create and use your own parkour structures +- **Visual Themes** - Customize block styles and particle effects +- **Leaderboards** - Track high scores with MySQL or local storage +- **Rewards System** - Configure rewards for score milestones +- **Multi-language** - Full localization support + +## 🎮 Game Modes + +- **Default** - Classic infinite parkour +- **Speedrun** - Blocks disappear after a few seconds +- **Hardcore** - Lose all rewards on fall +- **Gravity Shift** - Random gravity effects (levitation, slow falling, jump boost) +- **Elytra** - Fly through particle rings + +## 🗂️ Schematics + +LoParkour uses `.lpschem` format for custom parkour structures: +- Lightweight JSON-based format with GZIP compression +- Supports block states, markers (start/end points), and metadata +- Automatic conversion from old vilib format: `/lp schematic convert` +- Create schematics in-game with `/lp schematic save ` +- Reload schematics: `/lp schematic reload` + +## 🔧 Commands + +- `/lp join` - Join parkour +- `/lp leave` - Leave parkour +- `/lp spectate ` - Spectate another player +- `/lp schematic ` - Manage schematics +- `/lp forcejoin ` - Force player to join (admin) +- `/lp reset` - Reset leaderboards (admin) + +## 📊 Configuration + +Main config: `plugins/LoParkour/config.yml` +- Enable/disable features +- Configure game modes +- Set up MySQL connection +- Customize generation settings +- Configure rewards + +## 🌍 Localization + +Add custom languages in `plugins/LoParkour/locales/` +- Default: English (`en.yml`) +- Included: Russian (`ru.yml`) +- Players can switch language in-game menu + +## 🔌 Integrations + +- **PlaceholderAPI** - Use parkour stats in other plugins +- **Vault** - Economy rewards support +- **HolographicDisplays** - Holographic leaderboards +- **Floodgate** - Bedrock player support + +## 📝 Development + +Built with LoLib 2.0 framework for modern Minecraft plugin development. + +See `docs/CHANGES.md` for changelog. diff --git a/TODO.md b/TODO.md index 579861f..f035353 100644 --- a/TODO.md +++ b/TODO.md @@ -1,309 +1,248 @@ -# LoParkour — Мастер-план задач +# LoParkour — TODO & Roadmap -> Версия: 1.3.0 → 2.0.0 -> Обновлено: 2026-03-01 -> -> Цель: сделать лучше Infinite Parkour, Infinite Parkour Plus и Infinite Elytra Parkour — -> взять всё хорошее, устранить все их минусы, добавить то что их пользователи просили. +> **Принципы:** KISS · DRY · SOLID +> **Версия:** 1.3.0 → 2.0.0 +> **Обновлено:** 2026-03-05 --- -## 🔥 КРИТИЧНО — Краш при старте +## 📊 Статус миграции vilib → LoLib 2.0 -**Причина:** `instance` в `LoParkour.java` объявлен но никогда не присваивается. -`Config` enum при статической инициализации зовёт `LoParkour.getInFolder()` → NPE → краш. +✅ **МИГРАЦИЯ ЗАВЕРШЕНА** (2026-03-01) +- Файлов изменено: 120+ +- Строк кода: 3000+ +- Ошибок исправлено: 250+ +- Размер jar: 655 КБ +- Все меню работают +- Схематики работают -**Фикс:** +--- + +## Легенда +- 🔴 **Критично** — баги/краши, нужно прямо сейчас +- 🟡 **Важно** — неработающие фичи +- 🟢 **Планово** — улучшения и новые фичи +- ✅ **Готово** + +--- + +## 🔴 Критично + +| # | Задача | Файл | Статус | +|---|--------|------|--------| +| 1 | GUIManager.init() не вызывался — клики не отменялись | `LoParkour.java` | ✅ | +| 2 | SQL race condition: CREATE TABLE до connect() | `StorageSQL.java` | ✅ | +| 3 | PreparedStatement leak в sendQuery() | `StorageSQL.java` | ✅ | +| 4 | Leaderboard читал данные до готовности SQL | `Leaderboard.java` | ✅ | +| 5 | Scoreboard не назначался игроку / обновление отключено | `ParkourUser.java` | ✅ | +| 6 | Только 16 уникальных entries в scoreboard (§0..§f) | `ParkourUser.java` | ✅ | +| 7 | NPE в World.setup() если createWorld() упал | `World.java` | ✅ | +| 8 | World.world не обновлялся при крэш-рестарте | `World.java` | ✅ | +| 9 | UUID сравнивался через == вместо .equals() | `ParkourUser.java` | ✅ | +| 10 | Async world creation вызывалось не на main thread | `LoParkour.java` | ✅ | +| 11 | StorageSQL.close() не проверяет connection == null | `StorageSQL.java` | ✅ | + +--- + +## 🟡 Важно — неработающие фичи + +### Партиклы +**Файл:** `generator/ParkourGenerator.java` → `generator/EffectManager.java` +- [x] Реализован базовый DOT/CIRCLE/BOX через `EffectManager` +- [ ] Протестировать все три режима в игре +- [ ] Убедиться что `ParticleUtil.circle()` принимает `List` + +### ConfigUpdater +**Файл:** `config/Config.java` +- [ ] Раскомментировать `ConfigUpdater.update(...)` (закомментировано с TODO) +- [ ] Убедиться что `ignoredSections` правильно пробрасываются + +### SpeedrunMode — блоки не синхронизированы с генератором +**Файл:** `mode/SpeedrunMode.java` +- [ ] Переопределить `tick()` в `SpeedrunGenerator` +- [ ] Блоки должны исчезать через `history`, а не независимо + +### HardcoreMode — сброс наград при падении +**Файл:** `mode/HardcoreMode.java` +- [ ] Переопределить `reset()` в `HardcoreGenerator` +- [ ] Очищать `player.collectedRewards` при `regenerate=true` + +--- + +## 🟢 Планово — новые фичи + +### ElytraMode — кольца +**Файл:** `mode/ElytraMode.java` +- [ ] `ElytraGenerator extends ParkourGenerator` — генерировать кольца из партиклов +- [ ] Детекция пролёта через кольцо (bounding box check в tick) +- [ ] Cooldown на фейерверки (буст) + +### GravityShiftMode — визуальные эффекты +**Файл:** `mode/GravityShiftMode.java` +- [ ] Title при смене эффекта +- [ ] Партиклы PORTAL при смене +- [ ] Звук при смене + +### Валидация прыжков +**Файл:** `generator/ParkourGenerator.java` +- [ ] Включить `JUMP_VALIDATION_ENABLED` из конфига в `selectNext()` +- [ ] Retry-loop: до 10 попыток если прыжок невозможен +- [ ] Использовать уже существующий `JumpValidator` + +### Специальные прыжки +**Файл:** `generator/JumpType.java` (создать) +- [ ] neo-jump (4 блока по диагонали) +- [ ] head-hitter (блок над головой) +- [ ] Интегрировать в `selectBlocks()` через шанс из конфига + +### Ghost система +**Файл:** `ghost/` +- [ ] Протестировать `GhostRecorder` и `GhostPlayer` +- [ ] Включить в конфиге `ghost-mode.enabled: true` +- [ ] Команда `/lp ghost ` + +### Мультиплеер режимы +- [ ] RaceMode (гонка — кто первый наберёт N очков) +- [ ] CoopMode (кооператив — общий счёт) + +### Локализация режимов +**Файлы:** `locales/en.yml`, `locales/ru.yml` +- [ ] gravity-shift: название эффекта, `effect-applied` +- [ ] elytra: `ring-passed`, `boost-ready`, `boost-cooldown` +- [ ] hardcore: `rewards-lost` + +--- + +## ✅ Архитектурный рефакторинг — завершён + +| Что сделано | Результат | +|-------------|-----------| +| `Command.java` → удалён (дубликат) | `LoParkourCommand.java` — единственный файл | +| `LoParkourCommand.java` (528 строк) | `command/PlayerCommandHandler` + `AdminCommandHandler` + `SchematicCommandHandler` + тонкий роутер | +| `Events.java` (268 строк) | `listener/PlayerConnectionListener` + `ParkourRestrictionListener` + `SchematicWandListener` | +| `ParkourGenerator.java` (666 строк) | Делегирует в `EffectManager` + `GeneratorTick` | +| `Option.java` (247 строк) | `SqlOptions` + `ParticleOptions` + `GenerationOptions` + тонкий `Option` | +| GUI-паттерн (13 меню) | Исправлены на правильный `create()` API | +| Scoreboard | Полная реализация через Team prefix API | +| SQL инициализация | `pendingTableCreations` queue + `onConnectCallbacks` | + +--- + +## 🎮 Чек-лист перед релизом + +- [ ] `./gradlew build` без ошибок и предупреждений +- [ ] Запуск сервера без ошибок в консоли +- [ ] Паркур создаётся и работает (прыжки, счёт, рекорд) +- [ ] Скорборд обновляется во время игры +- [ ] Все меню открываются корректно +- [ ] SQL: таблицы создаются, данные сохраняются/читаются +- [ ] Схематики: save → reload → paste работает +- [ ] Лидерборд: запись и чтение рекордов +- [ ] Частицы и звук работают в каждом режиме +- [ ] `/lp forcejoin`, `forceleave`, `reset`, `recoverinventory` + + +--- + +## 🔧 План рефакторинга больших файлов + +### Проблемные файлы (>200 строк): +- `generator/ParkourGenerator.java` (306) → разбить на 3 файла +- `storage/StorageSQL.java` (248) → разбить на 2 файла +- `session/Session.java` (236) → разбить на 2 файла +- `player/ParkourUser.java` (220) → разбить на 2 файла +- `player/ParkourPlayer.java` (202) → разбить на 2 файла + +### 1. ParkourGenerator.java → 3 файла +``` +generator/ +├── ParkourGenerator.java (150) - координация +├── GeneratorState.java (80) - состояние (score, history) +└── GeneratorLifecycle.java (80) - tick, reset, fall +``` + +### 2. StorageSQL.java → 2 файла +``` +storage/ +├── SQLConnection.java (120) - управление подключением +└── SQLRepository.java (130) - CRUD операции +``` + +### 3. Session.java → 2 файла +``` +session/ +├── Session.java (120) - основная логика +└── SessionMembers.java (120) - управление участниками +``` + +### 4. ParkourUser.java → 2 файла +``` +player/ +├── ParkourUser.java (120) - базовый user +└── UserScoreboard.java (100) - scoreboard логика +``` + +### 5. ParkourPlayer.java → 2 файла +``` +player/ +├── ParkourPlayer.java (120) - gameplay +└── PlayerSettings.java (80) - настройки +``` + +--- + +## 🎨 Новые фичи (Фаза 3-6) + +### Визуальные темы ```java -@Override -public void onEnable() { - instance = this; // ← этого нет - super.onEnable(); // вызывает enable() +public enum VisualTheme { + NEON, // неоновые блоки + яркие партиклы + NATURE, // трава, листья, цветы + SPACE, // космос, звёзды + UNDERWATER,// вода, кораллы + HELL // нижний мир, лава } ``` -- [ ] Добавить `instance = this` перед `super.onEnable()` -- [ ] Пересобрать: `./gradlew shadowJar` -- [ ] Проверить что сервер стартует без ошибок +### Combo система +- Множитель очков за последовательные прыжки (x2, x3... x10) +- Визуальный индикатор combo +- Combo break при падении +- Бонусные очки за высокий combo + +### Специальные прыжки +- **Neo-jump** - 4 блока по диагонали (5% шанс) +- **Head-hitter** - блок над головой (10% шанс) +- **Ladder jump** - прыжок на лестницу (15% шанс) + +### Replay система +- Запись последнего прохождения +- Просмотр replay с камерой +- Экспорт/импорт replay файлов +- Замедление/ускорение воспроизведения --- -## 🚧 ВЫСОКИЙ ПРИОРИТЕТ — Сломано +## 📅 Временная шкала (6-7 недель) -### 1. Меню система (16 файлов отключены) -Все `open()` закомментированы. Игроки видят "Меню недоступно". - -- [ ] Изучить LoLib InventoryGUI API (`api_docs/guides/gui.md`) -- [ ] Базовый `BaseMenu` поверх LoLib `InventoryGUI` -- [ ] Мигрировать по приоритету: - - [ ] MainMenu - - [ ] PlayMenu / SingleMenu - - [ ] SettingsMenu / ParkourSettingsMenu - - [ ] LangMenu - - [ ] LeaderboardsMenu / SingleLeaderboardMenu - - [ ] LobbyMenu / PlayerManagementMenu - - [ ] CommunityMenu / SpectatorMenu - -### 2. Схематики (генерация паркура сломана) -`schematic.paste()` закомментирован — схематичные прыжки не генерируются. - -- [ ] Починить `Schematics.getSchematic()` вызовы -- [ ] Починить `schematic.paste()` / `rotatedPaste()` в `ParkourGenerator` -- [ ] Протестировать генерацию в игре +| Фаза | Задачи | Время | +|------|--------|-------| +| 0 | Критические баги | 1-2 дня | +| 1 | Рефакторинг файлов | 1 неделя | +| 2 | Исправление фич | 1 неделя | +| 3 | Новые режимы | 2 недели | +| 4 | Визуал (темы, combo) | 1 неделя | +| 5 | Ghost система | 3 дня | +| 6 | Replay система | 1 неделя | --- -## 🛠 РЕФАКТОРИНГ +## 🎯 Критерии релиза 2.0.0 -### 3. Config enum — антипаттерн -Конструктор обращается к синглтону при статической инициализации. - -- [ ] Убрать `LoParkour.getInFolder()` из конструктора -- [ ] Добавить lazy `getPath()` или явный `Config.init()` вызов - -### 4. `enable()` — разбить на методы (SRP) -```java -public void enable() { - instance = this; - loadConfigs(); - loadSchematics(); - registerModes(); - registerHooks(); - setupWorld(); - registerListeners(); - setupMetrics(); -} -``` -- [ ] Каждый private метод — одна задача, макс 20 строк - -### 5. Инкапсуляция -`session.generator`, `pp.session`, `pp.player` — публичные поля. -- [ ] Добавить геттеры, сделать поля private/protected - -### 6. Логирование -`logger.severe("msg - " + ex.getMessage())` теряет стек вызовов. -- [ ] Заменить на `loLogger().error("msg", ex)` везде - ---- - -## 🐛 БАГИ КОНКУРЕНТОВ — Устранить у нас лучше - -Эти проблемы убили Infinite Parkour в глазах сообщества. У нас их быть не должно. - -### Ghost blocks — блоки остаются висеть после сессии -У IP это главная жалоба — после дисконнекта или краша блоки паркура остаются в мире. - -- [ ] Аудит `ParkourGenerator.disable()` и `World.delete()` — всё ли чистится -- [ ] Список всех размещённых блоков писать в сессию, чистить при любом выходе -- [ ] Обработать `PlayerQuitEvent`, `PlayerKickEvent`, `ServerShutdownEvent` -- [ ] Тест: войти → выйти через Alt+F4 → зайти снова → блоков нет - -### World generation баг — генерируется лишний overworld -У IP при определённых условиях создавался новый мир и жрал память. - -- [ ] Убедиться что `World.create()` не создаёт новый мир если уже есть -- [ ] Проверить что мир регистрируется в `worlds` конфиге и не пересоздаётся -- [ ] Логировать создание/удаление мира - -### Scoreboard баг -У IP scoreboard ломался и фиксился только отключением. - -- [ ] Использовать LoLib Sidebar API вместо ручного scoreboard -- [ ] Обернуть в try/catch, при ошибке — пересоздать sidebar, не ронять плагин - -### Лидерборды из коробки без плагинов -У IP нужен HolographicDisplays, документация невнятная, люди не разбирались. - -- [ ] Лидерборды через встроенный LoLib holograms (пакеты, без зависимостей) -- [ ] `HoloHook` только как опциональное улучшение -- [ ] Авто-создание голограммы командой `/lp leaderboard create ` -- [ ] Документация прямо в `config.yml` комментариями - -### Inventory recovery -У IP терялся инвентарь при краше. Было, но работало криво. - -- [ ] Команда `/lp recoverinventory` — восстановить инвентарь из сохранённого снапшота -- [ ] Снапшот сохранять async на диск при входе в паркур -- [ ] Восстанавливать автоматически при старте если игрок был в паркуре - ---- - -## 🎮 РЕЖИМЫ — Доработка существующих - -### ElytraMode → полноценный Elytra Parkour -Сейчас: выдаётся элитра + ракеты, летишь над обычными блоками. Это не режим. -Нужно: 3 подрежима по аналогии с Infinite Elytra Parkour (6 режимов). - -**Подрежим 1 — Classic (бесконечный маршрут)** -- [ ] `ElytraGenerator` — генерирует широкие платформы и коридоры для полёта -- [ ] Firefly буст при подборе ракеты в мире -- [ ] Счёт за дистанцию - -**Подрежим 2 — Precision (точность через кольца)** -- [ ] Кольца из стекла/стеклопанелей — процедурная генерация -- [ ] Кольца разного диаметра (большие → маленькие с прогрессом) -- [ ] Счёт за точность: пролетел по центру → 3 очка, по краю → 1 очко, пропустил → 0 -- [ ] Хитбокс кольца через регионы LoLib (Cuboid/Cylinder) - -**Подрежим 3 — Obstacle (уклонение)** -- [ ] Стены с дырами, колонны, горизонтальные балки -- [ ] Нарастающая скорость через эффект скорости -- [ ] Жизни (3 касания = конец) - -**Ветровые зоны (просили пользователи Elytra плагина)** -- [ ] Зоны ускорения (tailwind) — зелёные частицы, +скорость -- [ ] Зоны замедления (headwind) — красные частицы, -скорость -- [ ] Боковой ветер — толчок в сторону - -**Общее для ElytraMode:** -- [ ] Конфиг: стартовые ракеты, размер колец, скорость нарастания -- [ ] Scoreboard: текущий подрежим, очки, ракеты -- [ ] Нет стандартного `ParkourGenerator` — только свой `ElytraGenerator` - -### SpeedrunMode -- [ ] Текущий рекорд в scoreboard в реальном времени -- [ ] Сплиты: +δ время на 10/25/50/100 прыжках (зелёный если быстрее рекорда, красный если медленнее) -- [ ] Ghost replay при побитии рекорда - -### HardcoreMode -- [ ] Настройка количества жизней (1–5) в конфиге -- [ ] Индикация жизней в scoreboard (сердца) -- [ ] Опция: потеря наград при потере жизни vs только при нуле жизней - -### GravityShiftMode -- [ ] Предупреждение за 3 прыжка до смены (title на экране) -- [ ] Визуальный эффект смены (частицы + звук) -- [ ] Scoreboard показывает следующий эффект - ---- - -## 🆕 НОВЫЕ РЕЖИМЫ - -### LavaRush (просили пользователи IP) -Паркур с поднимающейся лавой снизу. - -- [ ] `LavaRushMode extends DefaultMode` -- [ ] Лава-блок (или FakeBlock) поднимается каждые N тиков -- [ ] Скорость подъёма нарастает с прогрессом -- [ ] Счёт = высота от начального уровня -- [ ] Конфиг: начальная скорость, нарастание, визуальный тип (настоящая лава / красные частицы) -- [ ] Совет: FakeBlock через пакеты (LoLib packets) — не грузит сервер - -### LobbyParkour (Infinite Parkour Plus фича) -Фоновый паркур в зоне ожидания сервера. Игрок просто прыгает пока ждёт. - -- [ ] Отдельный режим без очков и лидерборда -- [ ] Не сохраняет статистику -- [ ] Настройка зоны в конфиге (мир + координаты) -- [ ] Авто-старт при входе в зону - -### SessionLobby — меню активных сессий (IP Plus фича) -Меню где видно кто сейчас играет, можно присоединиться или зайти зрителем. - -- [ ] `SessionMenu extends BaseMenu` — список всех активных сессий -- [ ] Каждая сессия: режим, игроки, счёт, кнопки "Присоединиться" / "Смотреть" -- [ ] Обновление в реальном времени (каждые 2 секунды) -- [ ] Команда `/lp sessions` - -### NatureParkour (Infiniti Parkus стиль) -Расслабленный паркур по природной местности. Без таймера. - -- [ ] `NatureGenerator`: - - Блоки: трава, земля, дерево, листья, цветы, камень, мох - - Широкие платформы (2–4 блока), мостики из дерева, арки из листьев - - Биомные темы: лес / горы / пляж / снег (переключаются в настройках) -- [ ] Нет таймера — счёт за пройденные блоки -- [ ] Коллаборативный: 2–4 игрока вместе на одном паркуре - -### ZigZagMode -- [ ] Строго зигзаг (лево/право/лево/право) с нарастающей скоростью -- [ ] Только одиночные блоки, без схематик -- [ ] Тренировочный режим для стрейфов - -### DuelsMode (мультиплеер) -- [ ] 2 игрока, параллельные трассы, 60 секунд -- [ ] Кто больше прыжков — победил -- [ ] Саботаж каждые 20 прыжков: выбрать дебафф противнику (замедление / слепота / левитация) -- [ ] Очередь матчей - ---- - -## 📊 УЛУЧШЕНИЯ КАЧЕСТВА ЖИЗНИ - -### Настройки прямо в GUI во время игры (из IP) -- [ ] Кнопка в инвентаре "⚙ Настройки" открывает мини-меню -- [ ] Можно менять: стиль блоков, сложность схематик, звуки, частицы -- [ ] Без выхода из паркура - -### Прыжки — 6 типов как в IP (часть уже есть) -Текущие: обычные, схематики. Нужно добавить: -- [ ] Ледяные блоки (ice) — скользкие приземления -- [ ] Слябы (slab) — уменьшенная площадь -- [ ] Заборы (fence) — узкие столбики -- [ ] Стеклопанели (glass pane) — тонкие вертикальные - -### PAPI плейсхолдеры — расширить -Сейчас есть базовые. Добавить: -- [ ] `%loparkour_mode%` — текущий режим -- [ ] `%loparkour_score%` — текущий счёт -- [ ] `%loparkour_best_%` — лучший результат в режиме -- [ ] `%loparkour_rank_%` — позиция в лидерборде -- [ ] `%loparkour_elytra_score%` — счёт в Elytra -- [ ] `%loparkour_elytra_rank%` — ранг в Elytra - ---- - -## ⚡ ПРОИЗВОДИТЕЛЬНОСТЬ - -- [ ] `GhostManager` — лимит записей (не хранить всё бесконечно) -- [ ] Блочная история в `ParkourGenerator` — убедиться что cleanup работает каждые 100 тиков -- [ ] Все Scheduler-задачи: сохранять ссылку, отменять в `disable()` -- [ ] Async загрузка данных игрока при входе (сейчас синхронно?) -- [ ] ElytraGenerator — хитбоксы колец через регионы, не поблочная проверка - ---- - -## 🧪 ТЕСТИРОВАНИЕ - -- [ ] Старт сервера без ошибок -- [ ] Вход/выход — инвентарь восстанавливается -- [ ] Ghost blocks: выйти через Alt+F4 → зайти → блоков нет -- [ ] Все режимы: Default, Speedrun, Hardcore, GravityShift, Elytra -- [ ] Меню: открывается, кнопки работают -- [ ] Схематики: генерируются правильно -- [ ] Лидерборды: обновляются, голограмма создаётся командой -- [ ] `/lp recoverinventory` — работает после краша - ---- - -## 📦 ВЕРСИОНИРОВАНИЕ - -| Версия | Что входит | -|--------|------------| -| 1.3.1 | Фикс краша (instance), меню работают, схематики работают, ghost blocks чистятся | -| 1.4.0 | ElytraMode полноценный (3 подрежима + ветер), inventory recovery, лидерборды без зависимостей | -| 1.5.0 | LavaRush, ZigZagMode, NatureParkour, SessionLobby меню | -| 1.6.0 | DuelsMode, LobbyParkour, настройки в GUI во время игры | -| 2.0.0 | Все 6 типов прыжков, все подрежимы Elytra, полный рефакторинг | - ---- - -## 💡 ИДЕИ НА БУДУЩЕЕ - -- Сезонные события — тематика блоков (снег зимой, цветы летом) -- Пользовательские схематики — игроки создают и загружают свои -- Еженедельные челленджи с особыми наградами -- Discord webhook при побитии рекорда -- Анти-чит через Physics из LoLib (проверка скорости прыжков) -- Replay для зрителей через Ghost API - ---- - -## 📅 ПЛАН НА СЕГОДНЯ - -1. **[5 мин]** Фикс `instance = this` → собрать → убедиться что стартует -2. **[1–2 ч]** Починить меню (MainMenu + PlayMenu как минимум) -3. **[1 ч]** Починить схематики -4. **[30 мин]** Аудит ghost blocks — убедиться что чистятся при любом выходе -5. **[1 ч]** ElytraGenerator — кольца, хотя бы Precision подрежим -6. **[30 мин]** Рефакторинг `enable()` — разбить на методы +- ✅ Все файлы < 200 строк +- ✅ 5+ режимов игры +- ✅ Ghost система активна +- ✅ Визуальные темы +- ✅ Combo система +- ✅ Replay система +- ✅ Специальные прыжки diff --git a/build.gradle.kts b/build.gradle.kts index 2864df2..6763461 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -72,6 +72,10 @@ dependencies { // Spigot API compileOnly("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT") + // Adventure API (for Component) + compileOnly("net.kyori:adventure-api:4.14.0") + compileOnly("net.kyori:adventure-text-minimessage:4.14.0") + // LoLib Core implementation(files("libs/lolib-2.0.1.jar")) diff --git a/docs/CHANGES.md b/docs/CHANGES.md deleted file mode 100644 index 93ca328..0000000 --- a/docs/CHANGES.md +++ /dev/null @@ -1,74 +0,0 @@ -# Changelog - LoParkour - -## Version 1.1.0 (Current) - -### Added -- **Jump Validation System** - - Mathematical validation using Pythagorean theorem - - Maximum jump distance: 4.5 blocks - - Configurable safe ranges for X, Y, Z axes - - `JumpValidator` class for precise jump checking - -- **Jump Types System** - - NEO_JUMP - jumps around fences/walls - - HEAD_HITTER - jumps with blocks overhead - - FENCE_JUMP - jumps onto fences - - TRAPDOOR_JUMP - jumps onto trapdoors - - LADDER_JUMP - jumps onto ladders - - Each type has difficulty multiplier - - Configurable in config.yml - -- **Memory Optimization** - - Changed history storage from ArrayList to LinkedList - - Automatic block cleanup at 20+ blocks distance - - Periodic cleanup task every 100 ticks - - Configurable cleanup settings - -- **New Game Modes** - - Speedrun Mode - blocks disappear after 1.5 seconds - - Gravity Shift Mode - random potion effects every 10 jumps - - Hardcore Mode - full reset on fall with optional reward loss - -- **Ghost Mode System** - - Records top-3 player movements per mode - - Compression (only movements > 0.1 blocks) - - Playback using ArmorStand entities - - Binary format for efficient storage - - Configurable ghost settings - -- **Roguelike Perk System** - - Double Jump perk (1 use) - - Magnet perk (10 jumps, 20% distance reduction) - - Second Chance perk (1 use, teleport back 3 blocks) - - Shield perk (5 jumps, fall protection) - - Rarity system: Common (60%), Rare (30%), Epic (10%) - - Bonus blocks every 20 jumps - -- **New .lpschem Schematic Format** - - JSON + GZIP compression - - Palette encoding for optimization - - BlockData support (not just Material) - - Auto-detect LIME_WOOL (start) and RED_WOOL (end) - - Support for BlockDisplay entities - - Support for particle effects - - Ghost path data storage - - Converter from old vilib format - - New commands: save, paste, list, reload, convert - -### Changed -- Migrated build system from Maven to Gradle -- Package renamed to dev.loki.loparkour -- Improved schematic command structure -- Updated all mode registrations - -### Technical -- Java 17 -- Spigot API 1.20.4 -- Gradle 8.1.1 -- Added Gson dependency for JSON serialization - ---- - -## Version 1.0.0 (Legacy) - -Initial release with basic parkour functionality. diff --git a/docs/LPSCHEM_USAGE.md b/docs/LPSCHEM_USAGE.md deleted file mode 100644 index 86f36dc..0000000 --- a/docs/LPSCHEM_USAGE.md +++ /dev/null @@ -1,177 +0,0 @@ -# LPSchematic - Примеры использования - -## Создание схематики из мира - -```java -// Выделить область в игре (WorldEdit style) -Location pos1 = player.getLocation(); -Location pos2 = player.getLocation().add(10, 5, 10); - -// Создать схематику -LPSchematic schematic = new LPSchematicBuilder("easy-jump-1", "Loki", 0.25, 10, 5, 10) - .fromWorld(world, pos1, pos2) - .addTag("easy") - .addTag("beginner") - .build(); - -// Сохранить -LPSchematicManager manager = new LPSchematicManager(); -manager.saveSchematic(schematic); -``` - -## Создание схематики вручную - -```java -LPSchematic schematic = new LPSchematicBuilder("custom-jump", "Loki", 0.5, 5, 3, 5) - .setBlock(0, 0, 0, Material.STONE) - .setBlock(1, 0, 1, Material.STONE) - .setBlock(2, 1, 2, Material.STONE) - .setBlock(3, 0, 3, Material.STONE) - .setBlock(4, 0, 4, Material.STONE) - .setStart(0, 0, 0) - .setEnd(4, 0, 4) - .addTag("medium") - .build(); - -manager.saveSchematic(schematic); -``` - -## Загрузка и вставка - -```java -LPSchematicManager manager = new LPSchematicManager(); -manager.loadAll(); - -LPSchematic schematic = manager.getSchematic("easy-jump-1"); -if (schematic != null) { - Location origin = player.getLocation(); - List blocks = schematic.paste(origin, world); - - player.sendMessage("Pasted " + blocks.size() + " blocks"); -} -``` - -## Добавление визуальных эффектов - -```java -LPSchematic schematic = manager.getSchematic("neon-city"); - -SchematicVisuals visuals = new SchematicVisuals(); - -// Добавить вращающийся золотой блок -SchematicVisuals.BlockDisplay display = new SchematicVisuals.BlockDisplay( - "minecraft:gold_block", - new float[]{5.5f, 2.0f, 5.5f}, - new float[]{0.8f, 0.8f, 0.8f} -); -display.animation = new SchematicVisuals.Animation("rotate", "y", 1.5f); -visuals.addDisplay(display); - -// Добавить частицы -SchematicVisuals.ParticleEffect particle = new SchematicVisuals.ParticleEffect( - "end_rod", - new float[]{5.0f, 3.0f, 5.0f}, - 5, - 0.2f -); -visuals.addParticle(particle); - -schematic.setVisuals(visuals); -manager.saveSchematic(schematic); -``` - -## Конвертация из старого формата - -```java -// TODO: Создать конвертер -public class SchematicConverter { - public static LPSchematic fromVilib(dev.efnilite.vilib.schematic.Schematic oldSchematic) { - // Извлечь данные из старой схематики - // Создать новую LPSchematic - // Вернуть результат - } -} -``` - -## Команда для создания схематики - -```java -@Override -public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - return false; - } - - Player player = (Player) sender; - - // /lpschem create - if (args[0].equals("create")) { - String name = args[1]; - double difficulty = Double.parseDouble(args[2]); - - // Получить выделенную область (из WorldEdit или своей системы) - Location pos1 = getPos1(player); - Location pos2 = getPos2(player); - - int width = Math.abs(pos2.getBlockX() - pos1.getBlockX()) + 1; - int height = Math.abs(pos2.getBlockY() - pos1.getBlockY()) + 1; - int length = Math.abs(pos2.getBlockZ() - pos1.getBlockZ()) + 1; - - LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), difficulty, width, height, length) - .fromWorld(player.getWorld(), pos1, pos2) - .build(); - - try { - manager.saveSchematic(schematic); - player.sendMessage("§aSchematic saved: " + name); - } catch (IOException e) { - player.sendMessage("§cFailed to save schematic"); - e.printStackTrace(); - } - - return true; - } - - return false; -} -``` - -## Формат файла (.lpschem) - -Файл сжат GZIP и содержит JSON: - -```json -{ - "format_version": 2, - "metadata": { - "name": "easy-jump-1", - "author": "Loki", - "difficulty": 0.25, - "tags": ["easy", "beginner"] - }, - "dimensions": { - "width": 5, - "height": 3, - "length": 5 - }, - "palette": [ - "minecraft:air", - "minecraft:stone", - "minecraft:oak_planks" - ], - "blocks": [0, 1, 1, 2, 0, 1, ...], - "markers": { - "start": {"x": 0, "y": 0, "z": 0}, - "end": {"x": 4, "y": 0, "z": 4}, - "checkpoints": [] - } -} -``` - -## Преимущества нового формата - -1. **Человекочитаемый** - можно редактировать в текстовом редакторе -2. **Компактный** - GZIP сжатие + palette encoding -3. **Расширяемый** - легко добавить новые поля -4. **Совместимый** - можно конвертировать в/из других форматов -5. **Современный** - поддержка BlockDisplay и визуальных эффектов diff --git a/docs/sd.md b/docs/sd.md deleted file mode 100644 index 7bf18e1..0000000 --- a/docs/sd.md +++ /dev/null @@ -1,220 +0,0 @@ -# Спецификация формата LoParkour Schematic (.lpschem) - -## 1. Общее описание - -**LoParkour Schematic** — это легковесный, расширяемый формат на базе JSON, предназначенный для хранения паркур-уровней в Minecraft. Формат оптимизирован для быстрой загрузки, сетевой передачи (для онлайн-редакторов) и поддержки современных визуальных элементов (Block Displays). - -### Основные характеристики -- **Расширение:** `.lpschem` -- **Кодировка:** UTF-8 -- **Сжатие:** GZIP (стандарт Java `GZIPOutputStream`) -- **Версия формата:** 2.0 - ---- - -## 2. Структура файла - -### 2.1. Основные секции - -```json -{ - "format_version": 2, - "metadata": { ... }, - "dimensions": { ... }, - "palette": [ ... ], - "blocks": [ ... ], - "markers": { ... }, - "visuals": { ... }, - "logic": { ... } -} -``` - -### 2.2. Пример полного файла - -```json -{ - "format_version": 2, - "metadata": { - "name": "Neon City Rush", - "author": "Loki", - "difficulty": 0.65, - "tags": ["urban", "hard", "neon"] - }, - "dimensions": { - "width": 10, - "height": 8, - "length": 10 - }, - "palette": [ - "minecraft:air", - "minecraft:black_concrete", - "minecraft:cyan_stained_glass", - "minecraft:oak_stairs[facing=north,half=bottom]" - ], - "blocks": [0, 1, 1, 2, 0, 3], - "markers": { - "start": {"x": 1, "y": 1, "z": 1}, - "end": {"x": 9, "y": 7, "z": 9}, - "checkpoints": [{"x": 5, "y": 4, "z": 5}] - }, - "visuals": { - "displays": [ - { - "type": "block", - "block": "minecraft:gold_block", - "pos": [5.5, 2.0, 5.5], - "scale": [0.8, 0.8, 0.8], - "animation": {"type": "rotate", "axis": "y", "speed": 1.5} - } - ], - "particles": [ - {"type": "end_rod", "pos": [5.0, 3.0, 5.0], "amount": 5, "spread": 0.2} - ] - }, - "logic": { - "ghost_path": [[1,1,1], [2,1,3], [4,2,5]] - } -} -``` - ---- - -## 3. Детальное описание полей - -### 3.1. metadata (обязательное) -Метаданные схематики. - -| Поле | Тип | Описание | -|------|-----|----------| -| `name` | string | Название уровня | -| `author` | string | Автор | -| `difficulty` | float | Сложность (0.0 - 1.0) | -| `tags` | string[] | Метки для поиска | - -### 3.2. dimensions (обязательное) -Размеры уровня. - -| Поле | Тип | Описание | -|------|-----|----------| -| `width` | int | Ширина по оси X | -| `height` | int | Высота по оси Y | -| `length` | int | Длина по оси Z | - -### 3.3. palette (обязательное) -Уникальный список используемых блоков. Индекс `0` всегда зарезервирован за `minecraft:air`. - -### 3.4. blocks (обязательное) -Одномерный массив индексов блоков. Размер: `width × height × length`. - -Формула индекса: -`index = x + (z × width) + (y × width × length)` - -### 3.5. markers (обязательное) -Ключевые точки уровня. - -| Поле | Тип | Описание | -|------|-----|----------| -| `start` | object | Координаты старта | -| `end` | object | Координаты финиша | -| `checkpoints` | object[] | Массив чекпоинтов | - -Каждая точка содержит поля `x`, `y`, `z`. - -### 3.6. visuals (опциональное) -Визуальные эффекты. - -#### 3.6.1. displays (BlockDisplay) -Сущности для декораций в воздухе. - -| Поле | Тип | Описание | -|------|-----|----------| -| `type` | string | Тип дисплея (`block`) | -| `block` | string | Блок для отображения | -| `pos` | float[3] | Позиция (x, y, z) | -| `scale` | float[3] | Масштаб (x, y, z) | -| `animation` | object | Анимация (опционально) | - -Анимация: -| Поле | Тип | Описание | -|------|-----|----------| -| `type` | string | Тип (`rotate`) | -| `axis` | string | Ось вращения (`x`, `y`, `z`) | -| `speed` | float | Скорость вращения | - -#### 3.6.2. particles (опциональное) -Точки спавна частиц. - -| Поле | Тип | Описание | -|------|-----|----------| -| `type` | string | Тип частиц | -| `pos` | float[3] | Позиция | -| `amount` | int | Количество частиц | -| `spread` | float | Рассеивание | - -### 3.7. logic (опциональное) -Логические данные уровня. - -| Поле | Тип | Описание | -|------|-----|----------| -| `ghost_path` | int[][] | Путь призрака (массив координат) | - ---- - -## 4. Алгоритмы и оптимизация - -### 4.1. Palette Encoding -Вместо хранения полных строк `BlockData` для каждого блока используется уникальный список в `palette`. В `blocks` хранятся только числовые индексы. - -**Результат:** Сокращение размера файла в 5-10 раз для повторяющихся структур. - -### 4.2. 1D Array Flattening -Одномерный массив обеспечивает быстрый доступ к блокам без вложенных объектов. - -### 4.3. Sparse Storage -Индекс `0` всегда зарезервирован за `minecraft:air`. При загрузке блоки воздуха можно игнорировать, что ускоряет вставку. - ---- - -## 5. Визуальные эффекты (Killer Features) - -### 5.1. Block Displays -- Висят в воздухе (не по сетке блоков) -- Любого размера (scale) -- Плавное вращение/движение без модов - -### 5.2. Частицы -Один асинхронный поток для обработки всех эффектов активных уровней. - ---- - -## 6. Онлайн-редактор (Web Concept) - -### Технологии -- **Frontend:** Three.js или React-Three-Fiber -- **Asset Loading:** Текстуры блоков из Minecraft Assets -- **Интерфейс:** Рисование блоков, расстановка чекпоинтов, предпросмотр анимаций - ---- - -## 7. Техника безопасности - -| Проблема | Решение | -|----------|---------| -| Несовместимость версий | Проверять `format_version`, уведомлять об обновлении | -| Missing Blocks | Заменять на `MAGENTA_GLAZED_TERRACOTTA` | -| Entity Leak | Хранить UUID дисплеев, удалять при выгрузке | -| Lag Spikes | Лимитировать `visuals` (макс. 50 дисплеев на уровень) | - ---- - -## 8. План реализации (Roadmap) - -1. [ ] Создать класс `LPSchematic` с поддержкой `GSON` -2. [ ] Написать `PaletteManager` для автоматической сборки уникальных блоков -3. [ ] Реализовать вставку блоков через `EditSession` или Bukkit API -4. [ ] Добавить поддержку `BlockDisplay` через пакеты (1.19.4+) -5. [ ] Написать конвертер из старого `.dat` / Java Serialization формата - ---- - -*Документация подготовлена для проекта LoParkour. 2026 г.* diff --git a/src/main/java/dev/loki/loparkour/Command.java b/src/main/java/dev/loki/loparkour/Command.java index 308c04b..93fc411 100644 --- a/src/main/java/dev/loki/loparkour/Command.java +++ b/src/main/java/dev/loki/loparkour/Command.java @@ -1,645 +1,7 @@ package dev.loki.loparkour; -import org.bukkit.Bukkit; - -import java.util.ArrayList; - -import dev.loki.loparkour.util.Item; -import dev.loki.loparkour.util.ParticleData; - -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.mode.MultiMode; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.player.data.InventoryData; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; -import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder; -import dev.loki.loparkour.schematic.lpschem.SchematicConverter; -import dev.loki.loparkour.session.Session; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.TabCompleter; - -import dev.loki.loparkour.util.ParticleUtil; - -import dev.loki.loparkour.util.Locations; -import dev.loki.loparkour.util.ColorUtil; -import org.bukkit.*; -import org.bukkit.command.BlockCommandSender; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.BoundingBox; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -@SuppressWarnings("deprecation") -public class Command implements CommandExecutor, TabCompleter { - - public static final HashMap selections = new HashMap<>(); - - private static final ItemStack WAND = new Item(Material.GOLDEN_AXE, "LPSchematic Wand") - .lore("Left click: first position", "Right click: second position") - .build(); - - @Override - public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String label, String[] args) { - Player player = null; - if (sender instanceof Player) { - player = (Player) sender; - } - - switch (args.length) { - case 0 -> handle0Args(sender, player); - case 1 -> handle1Args(args[0], sender, player); - case 2 -> handle2Args(args[0], args[1], sender, player); - case 3 -> handle3Args(args[0], args[1], args[2], sender, player); - } - return true; - } - - @Override - public List onTabComplete(CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) { - List completions = new ArrayList<>(); - switch (args.length) { - case 1 -> { - if (ParkourOption.JOIN.mayPerform(sender)) { - completions.add("join"); - completions.add("leave"); - } - if (ParkourOption.MAIN.mayPerform(sender)) { - completions.add("menu"); - } - if (ParkourOption.PLAY.mayPerform(sender)) { - completions.add("play"); - } - if (ParkourOption.LEADERBOARDS.mayPerform(sender)) { - completions.add("leaderboard"); - } - if (sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("schematic"); - completions.add("reload"); - completions.add("forcejoin"); - completions.add("forceleave"); - completions.add("reset"); - completions.add("recoverinventory"); - } - return completions(args[0], completions); - } - case 2 -> { - if (args[0].equalsIgnoreCase("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("everyone"); - for (ParkourPlayer pp : ParkourPlayer.getPlayers()) { - completions.add(pp.getName()); - } - } else if (args[0].equalsIgnoreCase("join") && sender.hasPermission(ParkourOption.JOIN.permission)) { - for (ParkourPlayer pp : ParkourPlayer.getPlayers()) { - completions.add(pp.getName()); - } - } else if (args[0].equalsIgnoreCase("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste")); - } else if (args[0].equalsIgnoreCase("forcejoin") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("nearest"); - completions.add("everyone"); - - for (Player pl : Bukkit.getOnlinePlayers()) { - completions.add(pl.getName()); - } - } else if (args[0].equalsIgnoreCase("forceleave") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("everyone"); - - for (Player pl : Bukkit.getOnlinePlayers()) { - completions.add(pl.getName()); - } - } else if (args[0].equalsIgnoreCase("recoverinventory") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - for (Player pl : Bukkit.getOnlinePlayers()) { - completions.add(pl.getName()); - } - } - return completions(args[1], completions); - } - default -> { - return Collections.emptyList(); - } - } - } - - private void handle0Args(@NotNull CommandSender sender, @Nullable Player player) { - if (player != null && ParkourOption.MAIN.mayPerform(player)) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - return; - } - sendHelpMessages(sender); - } - - private void sendHelpMessages(CommandSender sender) { - send(sender, ""); - send(sender, "--------------- %s ---------------".formatted(LoParkour.NAME)); - send(sender, ""); - send(sender, "/parkour - Main command"); - if (sender.hasPermission(ParkourOption.JOIN.permission)) { - send(sender, "/parkour join [mode/player] - Join the default mode or specify one."); - send(sender, "/parkour leave - Leave the game on this server"); - } - if (sender.hasPermission(ParkourOption.MAIN.permission)) { - send(sender, "/parkour menu - Open the menu"); - } - if (sender.hasPermission(ParkourOption.PLAY.permission)) { - send(sender, "/parkour play - Mode selection menu"); - } - if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) { - send(sender, "/parkour leaderboard [type]- Open the leaderboard of a mode"); - } - if (sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, "/LoParkour schematic - Create a schematic"); - send(sender, "/LoParkour reload - Reloads the messages-v3.yml file"); - send(sender, "/LoParkour reset - Resets all high scores. This can't be recovered!"); - send(sender, "/LoParkour forcejoin - Forces a specific player, the nearest or everyone to join"); - send(sender, "/LoParkour forceleave - Forces a specific player, the nearest or everyone to leave"); - send(sender, "/LoParkour recoverinventory - Recover a player's saved inventory. Useful for recovering data after server crashes or errors when leaving."); - } - send(sender, ""); - } - - private void handle1Args(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) { - switch (arg.toLowerCase()) { - case "help" -> sendHelpMessages(sender); - case "reload" -> { - if (!cooldown(sender, "reload", 2500)) { - return; - } - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - Config.reload(false); - - send(sender, "%sReloaded config files.".formatted(LoParkour.PREFIX)); - } - } - - if (player == null) { - return; - } - - switch (arg.toLowerCase()) { - case "join" -> { - if (!cooldown(sender, "join", 2500)) { - return; - } - - if (!ParkourOption.JOIN.mayPerform(player)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - ParkourUser user = ParkourUser.getUser(player); - if (user != null) { - return; - } - - Modes.DEFAULT.create(player); - } - case "play" -> { - if (ParkourOption.PLAY.mayPerform(player)) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } - } - case "leave" -> { - if (!cooldown(sender, "leave", 2500)) { - return; - } - ParkourUser.leave(player); - } - case "menu", "main" -> { - if (!ParkourOption.MAIN.mayPerform(player)) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } - } - case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid"); - case "schematic" -> { - if (!player.hasPermission(ParkourOption.ADMIN.permission)) { // default players shouldn't have access even if perms are disabled - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - send(player, ""); - send(player, "/LoParkour schematic wand - Get the schematic wand"); - send(player, "/LoParkour schematic pos1 - Set the first position of your selection"); - send(player, "/LoParkour schematic pos2 - Set the second position of your selection"); - send(player, "/LoParkour schematic save - Save selection to .lpschem format"); - send(player, "/LoParkour schematic paste - Paste a .lpschem schematic"); - send(player, "/LoParkour schematic list - List all loaded schematics"); - send(player, "/LoParkour schematic reload - Reload all schematics"); - send(player, "/LoParkour schematic convert - Convert old schematics to .lpschem"); - send(player, ""); - send(player, "Have any questions or need help? Join the Discord."); - } - } - } - - private void handle2Args(@NotNull String arg1, @NotNull String arg2, @NotNull CommandSender sender, @Nullable Player player) { - switch (arg1.toLowerCase()) { - case "forcejoin" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - if (arg2.equalsIgnoreCase("everyone")) { - Bukkit.getOnlinePlayers().forEach(other -> Modes.DEFAULT.create(other)); - send(sender, LoParkour.PREFIX + "Successfully force joined everyone!"); - return; - } - - if (arg2.equalsIgnoreCase("nearest")) { - Player closest = null; - double distance = Double.MAX_VALUE; - - // if player is found get location from player - // if no player is found, get location from command block - // if no command block is found, return null - Location from = sender instanceof Player ? ((Player) sender).getLocation() : (sender instanceof BlockCommandSender ? ((BlockCommandSender) sender).getBlock().getLocation() : null); - - if (from == null || from.getWorld() == null) { - return; - } - - // get the closest player - for (Player p : from.getWorld().getPlayers()) { - double d = p.getLocation().distance(from); - - if (d < distance) { - distance = d; - closest = p; - } - } - - // no closest player found - if (closest == null) { - return; - } - - send(sender, LoParkour.PREFIX + "Successfully force joined " + closest.getName() + "!"); - Modes.DEFAULT.create(closest); - return; - } - - Player other = Bukkit.getPlayer(arg2); - if (other == null) { - send(sender, LoParkour.PREFIX + "That player isn't online!"); - return; - } - - Modes.DEFAULT.create(other); - } - case "forceleave" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - if (arg2.equalsIgnoreCase("everyone")) { - ParkourPlayer.getPlayers().forEach(ParkourUser::leave); - send(sender, LoParkour.PREFIX + "Successfully force kicked everyone!"); - return; - } - - Player other = Bukkit.getPlayer(arg2); - if (other == null) { - send(sender, LoParkour.PREFIX + "That player isn't online!"); - return; - } - - ParkourUser user = ParkourUser.getUser(other); - if (user == null) { - send(sender, LoParkour.PREFIX + "That player isn't currently playing!"); - return; - } - - ParkourUser.leave(user); - } - case "reset" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission) || !cooldown(sender, "reset", 2500)) { - return; - } - - if (arg2.equalsIgnoreCase("everyone")) { - for (Mode mode : Registry.getModes()) { - Leaderboard leaderboard = mode.getLeaderboard(); - - if (leaderboard == null) { - continue; - } - - leaderboard.resetAll(); - leaderboard.write(true); - } - - send(sender, LoParkour.PREFIX + "Successfully reset all high scores in memory and the files."); - return; - } - String name = null; - UUID uuid = null; - - // Check online players - Player online = Bukkit.getPlayerExact(arg2); - if (online != null) { - name = online.getName(); - uuid = online.getUniqueId(); - } - - // Check uuid - if (arg2.contains("-")) { - uuid = UUID.fromString(arg2); - } - - // Check offline player - if (uuid == null) { - OfflinePlayer offline = Bukkit.getOfflinePlayer(arg2); - name = offline.getName(); - uuid = offline.getUniqueId(); - } - - UUID finalUuid = uuid; - String finalName = name; - - for (Mode mode : Registry.getModes()) { - Leaderboard leaderboard = mode.getLeaderboard(); - - if (leaderboard == null) { - continue; - } - - leaderboard.remove(finalUuid); - leaderboard.write(true); - } - - send(sender, LoParkour.PREFIX + "Successfully reset the high score of " + finalName + " in memory and the files."); - } - case "recoverinventory" -> { - if (!cooldown(sender, "recoverinventory", 2500) || !sender.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - Player other = Bukkit.getPlayer(arg2); - if (other == null) { - send(sender, LoParkour.PREFIX + "That player isn't online!"); - return; - } - - new InventoryData(other).load(result -> { - if (result != null) { - send(sender, "%sSuccessfully recovered the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName())); - send(sender, "%sGiving %s their items now...".formatted(LoParkour.PREFIX, other.getName())); - } else { - send(sender, "%sThere was an error recovering the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName())); - send(sender, "%s%s has no saved inventory or there was an error. Check the console.".formatted(LoParkour.PREFIX, other.getName())); - } - }); - } - } - - if (player == null) { - return; - } - - switch (arg1) { - case "join" -> { - if (!cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(player)) { - return; - } - - Mode mode = Registry.getMode(arg2); - - if (mode != null) { - mode.create(player); - return; - } - - Player other = Bukkit.getPlayer(arg2); - - if (other == null) { - send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default - return; - } - - ParkourPlayer parkourPlayer = ParkourPlayer.getPlayer(other); - - if (parkourPlayer == null) { - send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default - return; - } - - ParkourUser user = ParkourUser.getUser(player); - Session session = parkourPlayer.session; - if (user != null && user.session == session) { // already in same session - return; - } - - if (session.isAcceptingPlayers()) { - ((MultiMode) session.generator.getMode()).join(player, session); - } else { - Modes.SPECTATOR.create(player, session); - } - } - case "leaderboard" -> { - if (!ParkourOption.LEADERBOARDS.mayPerform(player)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - Mode mode = Registry.getMode(arg2.toLowerCase()); - - // if found gamemode is null, return to default - if (mode == null) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } else { - // TODO: Migrate to LoLib GUI - // Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } - } - case "schematic" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - Location playerLocation = player.getLocation(); - Location[] existingSelection = selections.get(player); - - switch (arg2.toLowerCase()) { - case "wand" -> { - player.getInventory().addItem(WAND); - - send(player, "----------- Schematics -----------"); - send(player, "Left click -> set first position | Right click -> set second position"); - send(player, "If you can't place a block and need to set a position mid-air, use /LoParkour schematic pos1/pos2 instead."); - } - case "pos1" -> { - send(player, "%sPosition 1 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true))); - - if (existingSelection == null) { - selections.put(player, new Location[]{playerLocation, null}); - return; - } - - selections.put(player, new Location[]{playerLocation, existingSelection[1]}); - - ParticleUtil.box(BoundingBox.of(playerLocation, existingSelection[1]), player.getWorld(), Particle.END_ROD, player, 0.2); - } - case "pos2" -> { - send(player, "%sPosition 2 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true))); - - if (existingSelection == null) { - selections.put(player, new Location[]{null, playerLocation}); - return; - } - - selections.put(player, new Location[]{existingSelection[0], playerLocation}); - - ParticleUtil.box(BoundingBox.of(existingSelection[0], playerLocation), player.getWorld(), Particle.END_ROD, player, 0.2); - } - case "save" -> { - if (!cooldown(sender, "LoParkour save schematic", 2500)) { - return; - } - - if (existingSelection == null || existingSelection[0] == null || existingSelection[1] == null) { - send(player, "Schematics Your schematic isn't complete yet. Make sure you've set the first and second position."); - return; - } - - String code = UUID.randomUUID().toString().split("-")[0]; - - send(player, ("Schematics Your schematic is being saved. It will use code '%s'. " + "You can change the code to whatever you like. " + "Don't forget to add this schematic to schematics.yml.").formatted(code)); - - // TODO: LPSchematic.save(file, pos1, pos2); - } - case "list" -> { - send(player, "Schematics Loaded schematics:"); - LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> { - send(player, "- " + name + " (" + schem.getMetadata().getDifficulty() + ")"); - }); - } - case "reload" -> { - if (!cooldown(sender, "LoParkour reload schematics", 2500)) { - return; - } - LoParkour.getSchematicManager().reload(); - send(player, "Schematics Reloaded all schematics."); - } - case "convert" -> { - if (!cooldown(sender, "LoParkour convert schematics", 5000)) { - return; - } - send(player, "Schematics Converting old schematics to .lpschem format..."); - SchematicConverter.convertAll(); - send(player, "Schematics Conversion complete! Check console for details."); - } - } - } - } - } - - private void handle3Args(@NotNull String arg1, @NotNull String arg2, @NotNull String arg3, @NotNull CommandSender sender, @Nullable Player player) { - if (player == null) { - return; - } - - if (arg1.equalsIgnoreCase("schematic")) { - if (!player.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - switch (arg2.toLowerCase()) { - case "save" -> { - if (!cooldown(sender, "LoParkour save schematic", 2500)) { - return; - } - - Location[] selection = selections.get(player); - if (selection == null || selection[0] == null || selection[1] == null) { - send(player, "Schematics Your schematic isn't complete yet. Make sure you've set the first and second position."); - return; - } - - try { - int width = Math.abs(selection[1].getBlockX() - selection[0].getBlockX()) + 1; - int height = Math.abs(selection[1].getBlockY() - selection[0].getBlockY()) + 1; - int length = Math.abs(selection[1].getBlockZ() - selection[0].getBlockZ()) + 1; - - LPSchematic schematic = new LPSchematicBuilder(arg3, player.getName(), 0.5, width, height, length) - .fromWorld(player.getWorld(), selection[0], selection[1]) - .build(); - - LoParkour.getSchematicManager().saveSchematic(schematic); - send(player, "Schematics Saved schematic as " + arg3 + "!"); - } catch (Exception e) { - send(player, "Schematics Failed to save schematic: " + e.getMessage()); - e.printStackTrace(); - } - } - case "paste" -> { - LPSchematic lpSchematic = LoParkour.getSchematicManager().getSchematic(arg3); - if (lpSchematic != null) { - lpSchematic.paste(player.getLocation(), player.getWorld()); - send(sender, "%sPasted .lpschem schematic %s".formatted(LoParkour.PREFIX, arg3)); - return; - } - - // TODO: Implement schematic loading with LoLib - LPSchematic oldSchematic = null; - if (oldSchematic == null) { - send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3)); - return; - } - - // oldSchematic.paste(player.getLocation(), player.getWorld()); - send(sender, "%sPasted old schematic %s".formatted(LoParkour.PREFIX, arg3)); - } - } - } - } - - private void send(CommandSender sender, String message) { - sender.sendMessage(ColorUtil.color(message)); - } - - private final Map cooldowns = new HashMap<>(); - - private boolean cooldown(CommandSender sender, String key, long millis) { - String fullKey = sender.getName() + ":" + key; - long now = System.currentTimeMillis(); - Long last = cooldowns.get(fullKey); - - if (last != null && now - last < millis) { - return false; - } - - cooldowns.put(fullKey, now); - return true; - } - - private List completions(String arg, List completions) { - return completions.stream() - .filter(s -> s.toLowerCase().startsWith(arg.toLowerCase())) - .toList(); - } -} +/** + * @deprecated Replaced by {@link LoParkourCommand}. Delete this file. + */ +@Deprecated +class Command extends LoParkourCommand {} diff --git a/src/main/java/dev/loki/loparkour/Events.java b/src/main/java/dev/loki/loparkour/Events.java index 36cf66c..6ff8a2e 100644 --- a/src/main/java/dev/loki/loparkour/Events.java +++ b/src/main/java/dev/loki/loparkour/Events.java @@ -1,328 +1,12 @@ package dev.loki.loparkour; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import dev.loki.loparkour.util.ParticleData; - -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.world.World; -import org.bukkit.event.Listener; - -import dev.loki.loparkour.util.ParticleUtil; -import dev.loki.loparkour.util.Locations; -import dev.loki.loparkour.util.ColorUtil; -import io.papermc.lib.PaperLib; -import org.bukkit.*; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.Cancellable; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.event.player.*; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.util.BoundingBox; -import org.jetbrains.annotations.ApiStatus; - -import java.time.Duration; -import java.time.Instant; -import java.util.NoSuchElementException; - /** - * Internal event handler + * @deprecated Split into: + * - {@link dev.loki.loparkour.listener.PlayerConnectionListener} + * - {@link dev.loki.loparkour.listener.ParkourRestrictionListener} + * - {@link dev.loki.loparkour.listener.SchematicWandListener} + * + * Delete this file after verifying compilation. */ -@ApiStatus.Internal -public class Events implements Listener { - - @EventHandler - public void chat(AsyncPlayerChatEvent event) { - if (!Option.OPTIONS_ENABLED.get(ParkourOption.CHAT)) { - return; - } - - Player player = event.getPlayer(); - ParkourUser user = ParkourUser.getUser(player); - - if (user == null) { - return; - } - - Session session = user.session; - - if (session.isMuted(user)) { - return; - } - - event.setCancelled(true); - switch (user.chatType) { - case LOBBY_ONLY -> session.getUsers().forEach(other -> other.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage())); - case PLAYERS_ONLY -> session.getPlayers().forEach(other -> other.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage())); - default -> event.setCancelled(false); - } - } - - @EventHandler(priority = EventPriority.LOWEST) - public void join(PlayerJoinEvent event) { - Player player = event.getPlayer(); - - if (Config.CONFIG.getBoolean("bungeecord.enabled")) { - Modes.DEFAULT.create(player); - return; - } - - if (!player.getWorld().equals(World.getWorld())) { - return; - } - - org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back")); - - if (fallback != null) { - PaperLib.teleportAsync(player, fallback.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); - return; - } - - PaperLib.teleportAsync(player, Bukkit.getWorlds().stream() - .filter(world -> !world.equals(World.getWorld())) - .findAny() - .orElseThrow(() -> new NoSuchElementException("No fallback world was found!")) - .getSpawnLocation()); - } - - @EventHandler - public void leave(PlayerQuitEvent event) { - ParkourUser user = ParkourUser.getUser(event.getPlayer()); - - if (user == null) { - return; - } - - ParkourUser.unregister(user, true, false, true); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void command(PlayerCommandPreprocessEvent event) { - if (!Config.CONFIG.getBoolean("focus-mode.enabled")) { - return; - } - - ParkourUser user = ParkourUser.getUser(event.getPlayer()); - - if (user == null) { - return; - } - - String command = event.getMessage().toLowerCase(); - if (Config.CONFIG.getStringList("focus-mode.whitelist").stream().anyMatch(c -> command.contains(c.toLowerCase()))) { - return; - } - - user.sendTranslated("other.no_do"); - event.setCancelled(true); - } - - @EventHandler - public void interactWand(PlayerInteractEvent event) { - Action action = event.getAction(); - Player player = event.getPlayer(); - ItemStack item = player.getInventory().getItemInMainHand(); - - if (!player.hasPermission("LoParkour.admin") || item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("LPSchematic Wand") || event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) { - return; - } - - Location location = event.getClickedBlock().getLocation(); - Location[] existingSelection = Command.selections.get(player); - - event.setCancelled(true); - - switch (action) { - case LEFT_CLICK_BLOCK -> { - send(player, LoParkour.PREFIX + "Position 1 was set to " + Locations.toString(location, true)); - - if (existingSelection == null) { - Command.selections.put(player, new Location[]{location, null}); - return; - } - - Command.selections.put(player, new Location[]{location, existingSelection[1]}); - - ParticleUtil.box(BoundingBox.of(location, existingSelection[1]), player.getWorld(), Particle.END_ROD, player, 0.2); - } - case RIGHT_CLICK_BLOCK -> { - send(player, LoParkour.PREFIX + "Position 2 was set to " + Locations.toString(location, true)); - - if (existingSelection == null) { - Command.selections.put(player, new Location[]{null, location}); - return; - } - - Command.selections.put(player, new Location[]{existingSelection[0], location}); - - ParticleUtil.box(BoundingBox.of(existingSelection[0], location), player.getWorld(), Particle.END_ROD, player, 0.2); - } - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void interact(PlayerInteractEvent event) { - Player player = event.getPlayer(); - ParkourPlayer pp = ParkourPlayer.getPlayer(player); - - if (pp == null) { - return; - } - - boolean type = event.getClickedBlock() != null && (event.getClickedBlock().getType() == Material.DISPENSER || event.getClickedBlock().getType() == Material.DROPPER || event.getClickedBlock().getType() == Material.HOPPER); - - if (event.getAction() == Action.RIGHT_CLICK_BLOCK && type && event.getHand() == EquipmentSlot.HAND) { - event.setCancelled(true); - player.closeInventory(); - return; - } - - boolean action = (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) && event.getHand() == EquipmentSlot.HAND; - - if (!action) { - return; - } - - Material held = getHeldItem(player).getType(); - - Material play = Locales.getItem(player, "play.item").build().getType(); - Material community = Locales.getItem(player, "community.item").build().getType(); - Material settings = Locales.getItem(player, "settings.item").build().getType(); - Material lobby = Locales.getItem(player, "lobby.item").build().getType(); - Material quit = Locales.getItem(player, "other.quit").build().getType(); - - event.setCancelled(true); - - if (held == play) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } else if (held == community) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } else if (held == settings) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } else if (held == lobby) { - // TODO: Migrate to LoLib GUI - // Menus.*.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } else if (held == quit) { - ParkourUser.leave(player); - } else { - if (!Config.CONFIG.getBoolean("options.disable-inventory-blocks")) { - event.setCancelled(false); - } - } - } - - private ItemStack getHeldItem(Player player) { - PlayerInventory inventory = player.getInventory(); - return inventory.getItemInMainHand().getType() == Material.AIR ? inventory.getItemInOffHand() : inventory.getItemInMainHand(); - } - - @EventHandler - public void switchWorld(PlayerChangedWorldEvent event) { - Player player = event.getPlayer(); - ParkourUser user = ParkourUser.getUser(player); - org.bukkit.World parkour = World.getWorld(); - - boolean isAdmin = Config.CONFIG.getBoolean("permissions.enabled") ? ParkourOption.ADMIN.mayPerform(player) : player.isOp(); - - if (player.getWorld() == parkour && user == null && !isAdmin && player.getTicksLived() > 20) { - Bukkit.getWorlds().stream() - .filter(world -> !world.equals(parkour)) - .findAny() - .ifPresent(world -> PaperLib.teleportAsync(player, world.getSpawnLocation())); - return; - } - - if (event.getFrom() == parkour && user != null && Duration.between(user.joined, Instant.now()).toMillis() > 100) { - ParkourUser.unregister(user, true, false, false); - } - } - - @EventHandler - public void onDrop(PlayerDropItemEvent event) { - handleRestriction(event.getPlayer(), event); - } - - @EventHandler - public void onPlace(BlockPlaceEvent event) { - handleRestriction(event.getPlayer(), event); - } - - @EventHandler - public void onBreak(BlockBreakEvent event) { - handleRestriction(event.getPlayer(), event); - } - - @EventHandler - public void damage(EntityDamageEvent event) { - if (!(event.getEntity() instanceof Player player)) { - return; - } - - handleRestriction(player, event); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void inventory(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player) || event.getInventory().getType() == InventoryType.CRAFTING) { - return; - } - - handleRestriction(player, event); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void spectate(PlayerTeleportEvent event) { - if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) { - return; - } - - handleRestriction(event.getPlayer(), event); - } - -// @EventHandler(priority = EventPriority.HIGHEST) -// public void mount(EntityMountEvent event) { -// if (!(event.getEntity() instanceof Player player)) { -// return; -// } -// -// handleRestriction(player, event); -// } - - private void handleRestriction(Player player, Cancellable event) { - if (!ParkourUser.isUser(player)) { - return; - } - - event.setCancelled(true); - } - - private void send(CommandSender sender, String message) { - sender.sendMessage(ColorUtil.color(message)); - } -} +@Deprecated +class Events {} diff --git a/src/main/java/dev/loki/loparkour/LoParkour.java b/src/main/java/dev/loki/loparkour/LoParkour.java index 33916e7..c61d470 100644 --- a/src/main/java/dev/loki/loparkour/LoParkour.java +++ b/src/main/java/dev/loki/loparkour/LoParkour.java @@ -19,6 +19,7 @@ import dev.loki.loparkour.schematic.lpschem.LPSchematicManager; import dev.loki.loparkour.storage.Storage; import dev.loki.loparkour.world.World; import dev.lolib.core.LoPlugin; +import dev.lolib.scheduler.Scheduler; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.bstats.bukkit.Metrics; @@ -28,6 +29,8 @@ import org.bstats.charts.SingleLineChart; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.util.ArrayList; +import java.util.List; /** * Main class of LoParkour @@ -39,7 +42,10 @@ public final class LoParkour extends LoPlugin { public static final String NAME = "&#FF6464LoParkour"; public static final String PREFIX = NAME + " 񢩈» &#A0A0A0"; - private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private static final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .excludeFieldsWithoutExposeAnnotation() + .create(); public static Gson getGson() { return gson; @@ -87,14 +93,9 @@ public final class LoParkour extends LoPlugin { - @Override - public void onEnable() { - instance = this; - super.onEnable(); - } - @Override public void enable() { + instance = this; // must be first — Config enum accesses instance inside reload() loadConfigs(); loadSchematics(); registerModes(); @@ -110,8 +111,77 @@ public final class LoParkour extends LoPlugin { private void loadSchematics() { schematicManager = new LPSchematicManager(); + + // Create default island schematic if it doesn't exist + File schematicsFolder = getInFolder("schematics-new"); + File islandFile = new File(schematicsFolder, "island.lpschem"); + + if (!islandFile.exists()) { + try { + createDefaultIslandSchematic(islandFile); + getLogger().info("Created default island schematic"); + } catch (Exception ex) { + getLogger().severe("Failed to create default island schematic: " + ex.getMessage()); + ex.printStackTrace(); + } + } + schematicManager.loadAll(); } + + private void createDefaultIslandSchematic(File file) throws Exception { + // Create a larger 9x5x9 island for better visibility and safety + List palette = new ArrayList<>(); + palette.add("minecraft:air"); + palette.add("minecraft:stone"); + palette.add("minecraft:diamond_block"); + palette.add("minecraft:emerald_block"); + + int width = 9, height = 5, length = 9; + int[] blocks = new int[width * height * length]; + + // Fill everything with air first + for (int i = 0; i < blocks.length; i++) { + blocks[i] = 0; + } + + // Create a solid stone platform at y=1 (7x7 platform) + for (int z = 1; z < 8; z++) { + for (int x = 1; x < 8; x++) { + int index = x + (z * width) + (1 * width * length); + blocks[index] = 1; // stone + } + } + + // Place diamond block (player spawn) at center + int centerX = 4, centerZ = 4; + int diamondIndex = centerX + (centerZ * width) + (1 * width * length); + blocks[diamondIndex] = 2; // diamond_block - player spawns here + + // Place emerald block (parkour start) to the right of spawn + int emeraldIndex = (centerX + 1) + (centerZ * width) + (1 * width * length); + blocks[emeraldIndex] = 3; // emerald_block - parkour starts here + + dev.loki.loparkour.schematic.lpschem.SchematicMetadata metadata = + new dev.loki.loparkour.schematic.lpschem.SchematicMetadata("island", "LoParkour", 0.0); + metadata.addTag("spawn"); + metadata.addTag("island"); + + dev.loki.loparkour.schematic.lpschem.SchematicDimensions dimensions = + new dev.loki.loparkour.schematic.lpschem.SchematicDimensions(width, height, length); + + // Markers point to the blocks where player spawns and parkour begins + dev.loki.loparkour.schematic.lpschem.SchematicMarkers markers = + new dev.loki.loparkour.schematic.lpschem.SchematicMarkers( + new dev.loki.loparkour.schematic.lpschem.SchematicMarkers.Vector3i(centerX, 1, centerZ), + new dev.loki.loparkour.schematic.lpschem.SchematicMarkers.Vector3i(centerX + 1, 1, centerZ) + ); + + dev.loki.loparkour.schematic.lpschem.LPSchematic schematic = + new dev.loki.loparkour.schematic.lpschem.LPSchematic(metadata, dimensions, palette, blocks, markers); + + schematic.save(file); + } private void registerModes() { Registry.register(new DefaultMode()); @@ -140,14 +210,31 @@ public final class LoParkour extends LoPlugin { } private void setupWorld() { - if (Config.CONFIG.getBoolean("joining")) { + if (!Config.CONFIG.getBoolean("joining")) { + return; + } + + // World creation MUST happen on the main thread (Bukkit API requirement) + try { + loLogger().info("Creating parkour world..."); World.create(); + loLogger().info("Parkour world created successfully!"); + } catch (Exception ex) { + loLogger().error("Failed to create parkour world", ex); } } private void registerEventsAndCommands() { - getServer().getPluginManager().registerEvents(new Events(), this); - getCommand("LoParkour").setExecutor(new Command()); + dev.lolib.gui.GUIManager.init(this); // required for LoLib GUI click handling + + var pm = getServer().getPluginManager(); + pm.registerEvents(new dev.loki.loparkour.listener.PlayerConnectionListener(), this); + pm.registerEvents(new dev.loki.loparkour.listener.ParkourRestrictionListener(), this); + pm.registerEvents(new dev.loki.loparkour.listener.SchematicWandListener(), this); + + LoParkourCommand cmd = new LoParkourCommand(); + getCommand("LoParkour").setExecutor(cmd); + getCommand("LoParkour").setTabCompleter(cmd); } private void setupMetrics() { diff --git a/src/main/java/dev/loki/loparkour/LoParkourCommand.java b/src/main/java/dev/loki/loparkour/LoParkourCommand.java index 3ee1c0c..3ac2111 100644 --- a/src/main/java/dev/loki/loparkour/LoParkourCommand.java +++ b/src/main/java/dev/loki/loparkour/LoParkourCommand.java @@ -1,11 +1,9 @@ package dev.loki.loparkour; -import org.bukkit.Bukkit; - -import java.util.ArrayList; - import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.command.AdminCommandHandler; +import dev.loki.loparkour.command.PlayerCommandHandler; +import dev.loki.loparkour.command.SchematicCommandHandler; import dev.loki.loparkour.config.Locales; import dev.loki.loparkour.leaderboard.Leaderboard; import dev.loki.loparkour.menu.Menus; @@ -15,613 +13,170 @@ import dev.loki.loparkour.mode.Modes; import dev.loki.loparkour.mode.MultiMode; import dev.loki.loparkour.player.ParkourPlayer; import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.player.data.InventoryData; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; -import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder; import dev.loki.loparkour.session.Session; import dev.loki.loparkour.util.ColorUtil; -import dev.loki.loparkour.util.ParticleUtil; -import dev.loki.loparkour.util.Locations; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; -import org.bukkit.command.TabCompleter; -import org.bukkit.*; -import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.BoundingBox; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +/** + * Main command router for /LoParkour and /parkour. + * Delegates to {@link PlayerCommandHandler}, {@link AdminCommandHandler}, + * and {@link SchematicCommandHandler}. + */ @SuppressWarnings("deprecation") public class LoParkourCommand implements CommandExecutor, TabCompleter { - public static final HashMap selections = new HashMap<>(); - - private static final ItemStack WAND = createWand(); - - private static ItemStack createWand() { - ItemStack wand = new ItemStack(Material.GOLDEN_AXE); - var meta = wand.getItemMeta(); - if (meta != null) { - meta.setDisplayName(ColorUtil.color("LPSchematic Wand")); - meta.setLore(List.of( - ColorUtil.color("Left click: first position"), - ColorUtil.color("Right click: second position") - )); - wand.setItemMeta(meta); - } - return wand; - } + private final PlayerCommandHandler player = new PlayerCommandHandler(); + private final AdminCommandHandler admin = new AdminCommandHandler(player); @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - Player player = null; - if (sender instanceof Player) { - player = (Player) sender; - } + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + Player p = sender instanceof Player ? (Player) sender : null; switch (args.length) { - case 0 -> handle0Args(sender, player); - case 1 -> handle1Args(args[0], sender, player); - case 2 -> handle2Args(args[0], args[1], sender, player); - case 3 -> handle3Args(args[0], args[1], args[2], sender, player); + case 0 -> player.handleNoArgs(sender, p); + case 1 -> player.handle(args[0], sender, p); + case 2 -> handle2(args[0], args[1], sender, p); + case 3 -> handle3(args[0], args[1], args[2], sender, p); } return true; } - @Override - public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - List completions = new ArrayList<>(); - switch (args.length) { - case 1 -> { - if (ParkourOption.JOIN.mayPerform(sender)) { - completions.add("join"); - completions.add("leave"); - } - if (ParkourOption.MAIN.mayPerform(sender)) { - completions.add("menu"); - } - if (ParkourOption.PLAY.mayPerform(sender)) { - completions.add("play"); - } - if (ParkourOption.LEADERBOARDS.mayPerform(sender)) { - completions.add("leaderboard"); - } - if (sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("schematic"); - completions.add("reload"); - completions.add("forcejoin"); - completions.add("forceleave"); - completions.add("reset"); - completions.add("recoverinventory"); - } - return completions(args[0], completions); - } - case 2 -> { - if (args[0].equalsIgnoreCase("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("everyone"); - for (ParkourPlayer pp : ParkourPlayer.getPlayers()) { - completions.add(pp.getName()); - } - } else if (args[0].equalsIgnoreCase("join") && sender.hasPermission(ParkourOption.JOIN.permission)) { - for (ParkourPlayer pp : ParkourPlayer.getPlayers()) { - completions.add(pp.getName()); - } - } else if (args[0].equalsIgnoreCase("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste")); - } else if (args[0].equalsIgnoreCase("forcejoin") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("nearest"); - completions.add("everyone"); + // ── 2-arg routing ────────────────────────────────────────────────────────── - for (Player pl : Bukkit.getOnlinePlayers()) { - completions.add(pl.getName()); - } - } else if (args[0].equalsIgnoreCase("forceleave") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.add("everyone"); - - for (Player pl : Bukkit.getOnlinePlayers()) { - completions.add(pl.getName()); - } - } else if (args[0].equalsIgnoreCase("recoverinventory") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - for (Player pl : Bukkit.getOnlinePlayers()) { - completions.add(pl.getName()); - } - } - return completions(args[1], completions); - } - default -> { - return Collections.emptyList(); - } - } - } - - private void handle0Args(@NotNull CommandSender sender, @Nullable Player player) { - if (player != null && ParkourOption.MAIN.mayPerform(player)) { - // TODO: Migrate to LoLib GUI - // Menus.MAIN.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - return; - } - sendHelpMessages(sender); - } - - private void sendHelpMessages(CommandSender sender) { - send(sender, ""); - send(sender, "--------------- %s ---------------".formatted(LoParkour.NAME)); - send(sender, ""); - send(sender, "/parkour - Main command"); - if (sender.hasPermission(ParkourOption.JOIN.permission)) { - send(sender, "/parkour join [mode/player] - Join the default mode or specify one."); - send(sender, "/parkour leave - Leave the game on this server"); - } - if (sender.hasPermission(ParkourOption.MAIN.permission)) { - send(sender, "/parkour menu - Open the menu"); - } - if (sender.hasPermission(ParkourOption.PLAY.permission)) { - send(sender, "/parkour play - Mode selection menu"); - } - if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) { - send(sender, "/parkour leaderboard [type]- Open the leaderboard of a mode"); - } - if (sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, "/LoParkour schematic - Create a schematic"); - send(sender, "/LoParkour reload - Reloads the messages-v3.yml file"); - send(sender, "/LoParkour reset - Resets all high scores. This can't be recovered!"); - send(sender, "/LoParkour forcejoin - Forces a specific player, the nearest or everyone to join"); - send(sender, "/LoParkour forceleave - Forces a specific player, the nearest or everyone to leave"); - send(sender, "/LoParkour recoverinventory - Recover a player's saved inventory. Useful for recovering data after server crashes or errors when leaving."); - } - send(sender, ""); - } - - private void handle1Args(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) { - switch (arg.toLowerCase()) { - case "help" -> sendHelpMessages(sender); - case "reload" -> { - if (!cooldown(sender, "reload", 2500)) { - return; - } - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - Config.reload(false); - - send(sender, "%sReloaded config files.".formatted(LoParkour.PREFIX)); - } - } - - if (player == null) { - return; - } - - switch (arg.toLowerCase()) { - case "join" -> { - if (!cooldown(sender, "join", 2500)) { - return; - } - - if (!ParkourOption.JOIN.mayPerform(player)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - ParkourUser user = ParkourUser.getUser(player); - if (user != null) { - return; - } - - Modes.DEFAULT.create(player); - } - case "play" -> { - if (ParkourOption.PLAY.mayPerform(player)) { - // TODO: Migrate to LoLib GUI - // Menus.PLAY.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } - } - case "leave" -> { - if (!cooldown(sender, "leave", 2500)) { - return; - } - ParkourUser.leave(player); - } - case "menu", "main" -> { - if (!ParkourOption.MAIN.mayPerform(player)) { - // TODO: Migrate to LoLib GUI - // Menus.MAIN.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } - } - case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid"); - case "schematic" -> { - if (!player.hasPermission(ParkourOption.ADMIN.permission)) { // default players shouldn't have access even if perms are disabled - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - send(player, ""); - send(player, "/LoParkour schematic wand - Get the schematic wand"); - send(player, "/LoParkour schematic pos1 - Set the first position of your selection"); - send(player, "/LoParkour schematic pos2 - Set the second position of your selection"); - send(player, "/LoParkour schematic save - Save selection to .lpschem format"); - send(player, "/LoParkour schematic paste - Paste a .lpschem schematic"); - send(player, "/LoParkour schematic list - List all loaded schematics"); - send(player, "/LoParkour schematic reload - Reload all schematics"); - send(player, ""); - send(player, "Have any questions or need help? Join the Discord."); - } - } - } - - private void handle2Args(@NotNull String arg1, @NotNull String arg2, @NotNull CommandSender sender, @Nullable Player player) { - switch (arg1.toLowerCase()) { - case "forcejoin" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - if (arg2.equalsIgnoreCase("everyone")) { - Bukkit.getOnlinePlayers().forEach(other -> Modes.DEFAULT.create(other)); - send(sender, LoParkour.PREFIX + "Successfully force joined everyone!"); - return; - } - - if (arg2.equalsIgnoreCase("nearest")) { - Player closest = null; - double distance = Double.MAX_VALUE; - - // if player is found get location from player - // if no player is found, get location from command block - // if no command block is found, return null - Location from = sender instanceof Player ? ((Player) sender).getLocation() : (sender instanceof BlockCommandSender ? ((BlockCommandSender) sender).getBlock().getLocation() : null); - - if (from == null || from.getWorld() == null) { - return; - } - - // get the closest player - for (Player p : from.getWorld().getPlayers()) { - double d = p.getLocation().distance(from); - - if (d < distance) { - distance = d; - closest = p; - } - } - - // no closest player found - if (closest == null) { - return; - } - - send(sender, LoParkour.PREFIX + "Successfully force joined " + closest.getName() + "!"); - Modes.DEFAULT.create(closest); - return; - } - - Player other = Bukkit.getPlayer(arg2); - if (other == null) { - send(sender, LoParkour.PREFIX + "That player isn't online!"); - return; - } - - Modes.DEFAULT.create(other); - } - case "forceleave" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - if (arg2.equalsIgnoreCase("everyone")) { - ParkourPlayer.getPlayers().forEach(ParkourUser::leave); - send(sender, LoParkour.PREFIX + "Successfully force kicked everyone!"); - return; - } - - Player other = Bukkit.getPlayer(arg2); - if (other == null) { - send(sender, LoParkour.PREFIX + "That player isn't online!"); - return; - } - - ParkourUser user = ParkourUser.getUser(other); - if (user == null) { - send(sender, LoParkour.PREFIX + "That player isn't currently playing!"); - return; - } - - ParkourUser.leave(user); - } - case "reset" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission) || !cooldown(sender, "reset", 2500)) { - return; - } - - if (arg2.equalsIgnoreCase("everyone")) { - for (Mode mode : Registry.getModes()) { - Leaderboard leaderboard = mode.getLeaderboard(); - - if (leaderboard == null) { - continue; - } - - leaderboard.resetAll(); - leaderboard.write(true); - } - - send(sender, LoParkour.PREFIX + "Successfully reset all high scores in memory and the files."); - return; - } - String name = null; - UUID uuid = null; - - // Check online players - Player online = Bukkit.getPlayerExact(arg2); - if (online != null) { - name = online.getName(); - uuid = online.getUniqueId(); - } - - // Check uuid - if (arg2.contains("-")) { - uuid = UUID.fromString(arg2); - } - - // Check offline player - if (uuid == null) { - OfflinePlayer offline = Bukkit.getOfflinePlayer(arg2); - name = offline.getName(); - uuid = offline.getUniqueId(); - } - - UUID finalUuid = uuid; - String finalName = name; - - for (Mode mode : Registry.getModes()) { - Leaderboard leaderboard = mode.getLeaderboard(); - - if (leaderboard == null) { - continue; - } - - leaderboard.remove(finalUuid); - leaderboard.write(true); - } - - send(sender, LoParkour.PREFIX + "Successfully reset the high score of " + finalName + " in memory and the files."); - } - case "recoverinventory" -> { - if (!cooldown(sender, "recoverinventory", 2500) || !sender.hasPermission(ParkourOption.ADMIN.permission)) { - return; - } - - Player other = Bukkit.getPlayer(arg2); - if (other == null) { - send(sender, LoParkour.PREFIX + "That player isn't online!"); - return; - } - - new InventoryData(other).load(result -> { - if (result != null) { - send(sender, "%sSuccessfully recovered the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName())); - send(sender, "%sGiving %s their items now...".formatted(LoParkour.PREFIX, other.getName())); - } else { - send(sender, "%sThere was an error recovering the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName())); - send(sender, "%s%s has no saved inventory or there was an error. Check the console.".formatted(LoParkour.PREFIX, other.getName())); - } - }); - } - } - - if (player == null) { - return; - } - - switch (arg1) { - case "join" -> { - if (!cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(player)) { - return; - } - - Mode mode = Registry.getMode(arg2); - - if (mode != null) { - mode.create(player); - return; - } - - Player other = Bukkit.getPlayer(arg2); - - if (other == null) { - send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default - return; - } - - ParkourPlayer parkourPlayer = ParkourPlayer.getPlayer(other); - - if (parkourPlayer == null) { - send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default - return; - } - - ParkourUser user = ParkourUser.getUser(player); - Session session = parkourPlayer.session; - if (user != null && user.session == session) { // already in same session - return; - } - - if (session.isAcceptingPlayers()) { - ((MultiMode) session.generator.getMode()).join(player, session); - } else { - Modes.SPECTATOR.create(player, session); - } - } - case "leaderboard" -> { - if (!ParkourOption.LEADERBOARDS.mayPerform(player)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - Mode mode = Registry.getMode(arg2.toLowerCase()); - - // if found gamemode is null, return to default - if (mode == null) { - // TODO: Migrate to LoLib GUI - // Menus.LEADERBOARDS.open(player); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } else { - // TODO: Migrate to LoLib GUI - // Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE); - player.sendMessage("§cМеню временно недоступно во время миграции"); - } - } - case "schematic" -> { - if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, Locales.getString(player, "other.no_do")); - return; - } - - Location playerLocation = player.getLocation(); - Location[] existingSelection = selections.get(player); - - switch (arg2.toLowerCase()) { - case "wand" -> { - player.getInventory().addItem(WAND); - - send(player, "----------- Schematics -----------"); - send(player, "Left click -> set first position | Right click -> set second position"); - send(player, "If you can't place a block and need to set a position mid-air, use /LoParkour schematic pos1/pos2 instead."); - } - case "pos1" -> { - send(player, "%sPosition 1 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true))); - - if (existingSelection == null) { - selections.put(player, new Location[]{playerLocation, null}); - return; - } - - selections.put(player, new Location[]{playerLocation, existingSelection[1]}); - - ParticleUtil.box(BoundingBox.of(playerLocation, existingSelection[1]), player.getWorld(), Particle.END_ROD, player, 0.2); - } - case "pos2" -> { - send(player, "%sPosition 2 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true))); - - if (existingSelection == null) { - selections.put(player, new Location[]{null, playerLocation}); - return; - } - - selections.put(player, new Location[]{existingSelection[0], playerLocation}); - - ParticleUtil.box(BoundingBox.of(existingSelection[0], playerLocation), player.getWorld(), Particle.END_ROD, player, 0.2); - } - case "save" -> { - if (!cooldown(sender, "LoParkour save schematic", 2500)) { - return; - } - - if (existingSelection == null || existingSelection[0] == null || existingSelection[1] == null) { - send(player, "Schematics Your schematic isn't complete yet. Make sure you've set the first and second position."); - return; - } - - String code = UUID.randomUUID().toString().split("-")[0]; - - send(player, ("Schematics Your schematic is being saved. It will use code '%s'. " + "You can change the code to whatever you like. " + "Don't forget to add this schematic to schematics.yml.").formatted(code)); - - // Old vilib schematic save - deprecated, use .lpschem format instead - send(player, "Warning: Old schematic format is deprecated. Use '/loparkour schematic save ' for .lpschem format."); - } - case "list" -> { - send(player, "Schematics Loaded schematics:"); - LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> { - send(player, "- " + name + " (" + schem.getMetadata().getDifficulty() + ")"); - }); - } - case "reload" -> { - if (!cooldown(sender, "LoParkour reload schematics", 2500)) { - return; - } - LoParkour.getSchematicManager().reload(); - send(player, "Schematics Reloaded all schematics."); - } - } - } - } - } - - private void handle3Args(@NotNull String arg1, @NotNull String arg2, @NotNull String arg3, @NotNull CommandSender sender, @Nullable Player player) { - if (player == null) { - return; - } - - if (arg1.equalsIgnoreCase("schematic")) { - if (!player.hasPermission(ParkourOption.ADMIN.permission)) { + private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) { + // Admin commands that don't need a player + switch (a1.toLowerCase()) { + case "forcejoin", "forceleave", "reset", "recoverinventory" -> { + admin.handle(a1, a2, sender, p); return; } + } - switch (arg2.toLowerCase()) { - case "save" -> { - if (!cooldown(sender, "LoParkour save schematic", 2500)) { - return; - } + if (p == null) return; - Location[] selection = selections.get(player); - if (selection == null || selection[0] == null || selection[1] == null) { - send(player, "Schematics Your schematic isn't complete yet. Make sure you've set the first and second position."); - return; - } - - try { - int width = Math.abs(selection[1].getBlockX() - selection[0].getBlockX()) + 1; - int height = Math.abs(selection[1].getBlockY() - selection[0].getBlockY()) + 1; - int length = Math.abs(selection[1].getBlockZ() - selection[0].getBlockZ()) + 1; - - LPSchematic schematic = new LPSchematicBuilder(arg3, player.getName(), 0.5, width, height, length) - .fromWorld(player.getWorld(), selection[0], selection[1]) - .build(); - - LoParkour.getSchematicManager().saveSchematic(schematic); - send(player, "Schematics Saved schematic as " + arg3 + "!"); - } catch (Exception e) { - send(player, "Schematics Failed to save schematic: " + e.getMessage()); - e.printStackTrace(); - } - } - case "paste" -> { - LPSchematic lpSchematic = LoParkour.getSchematicManager().getSchematic(arg3); - if (lpSchematic != null) { - lpSchematic.paste(player.getLocation(), player.getWorld()); - send(sender, "%sPasted .lpschem schematic %s".formatted(LoParkour.PREFIX, arg3)); - return; - } - - send(sender, "%sCouldn't find %s. Only .lpschem format is supported.".formatted(LoParkour.PREFIX, arg3)); + switch (a1.toLowerCase()) { + case "join" -> handleJoin(a2, sender, p); + case "leaderboard" -> handleLeaderboard(a2, sender, p); + case "schematic" -> { + if (!p.hasPermission(ParkourOption.ADMIN.permission)) { + send(sender, Locales.getString(p, "other.no_do")); + return; } + SchematicCommandHandler.handleSubcommand(a2, sender, p, player); } } } - private void send(CommandSender sender, String message) { - sender.sendMessage(ColorUtil.color(message)); + // ── 3-arg routing ────────────────────────────────────────────────────────── + + private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) { + if (p == null) return; + if (!a1.equalsIgnoreCase("schematic")) return; + if (!p.hasPermission(ParkourOption.ADMIN.permission)) return; + SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p, player); } - private List completions(String arg, List options) { - String lower = arg.toLowerCase(); - return options.stream() - .filter(option -> option.toLowerCase().startsWith(lower)) - .toList(); + // ── join logic ───────────────────────────────────────────────────────────── + + private void handleJoin(String arg, CommandSender sender, Player p) { + if (!player.cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(p)) return; + + Mode mode = Registry.getMode(arg); + if (mode != null) { mode.create(p); return; } + + Player other = Bukkit.getPlayer(arg); + if (other == null) { send(sender, LoParkour.PREFIX + "Unknown player."); return; } + + ParkourPlayer pp = ParkourPlayer.getPlayer(other); + if (pp == null) { send(sender, LoParkour.PREFIX + "That player isn't playing."); return; } + + ParkourUser user = ParkourUser.getUser(p); + Session session = pp.session; + if (user != null && user.session == session) return; + + if (session.isAcceptingPlayers()) { + ((MultiMode) session.generator.getMode()).join(p, session); + } else { + Modes.SPECTATOR.create(p, session); + } } - private boolean cooldown(CommandSender sender, String key, long millis) { - // Simple cooldown implementation - can be enhanced later - return true; + // ── leaderboard logic ────────────────────────────────────────────────────── + + private void handleLeaderboard(String arg, CommandSender sender, Player p) { + if (!ParkourOption.LEADERBOARDS.mayPerform(p)) { + send(sender, Locales.getString(p, "other.no_do")); + return; + } + Mode mode = Registry.getMode(arg.toLowerCase()); + if (mode == null) { + Menus.LEADERBOARDS.open(p); + } else { + Menus.SINGLE_LEADERBOARD.open(p, mode, Leaderboard.Sort.SCORE); + } + } + + // ── tab complete ─────────────────────────────────────────────────────────── + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String alias, @NotNull String[] args) { + List completions = new ArrayList<>(); + + if (args.length == 1) { + if (ParkourOption.JOIN.mayPerform(sender)) { completions.add("join"); completions.add("leave"); } + if (ParkourOption.MAIN.mayPerform(sender)) completions.add("menu"); + if (ParkourOption.PLAY.mayPerform(sender)) completions.add("play"); + if (ParkourOption.LEADERBOARDS.mayPerform(sender)) completions.add("leaderboard"); + if (sender.hasPermission(ParkourOption.ADMIN.permission)) { + completions.addAll(List.of("schematic", "reload", "forcejoin", "forceleave", "reset", "recoverinventory")); + } + return filter(args[0], completions); + } + + if (args.length == 2) { + String a1 = args[0].toLowerCase(); + if (a1.equals("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) { + completions.add("everyone"); + ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName())); + } else if (a1.equals("join") && ParkourOption.JOIN.mayPerform(sender)) { + ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName())); + } else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) { + completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste", "list", "reload")); + } else if ((a1.equals("forcejoin") || a1.equals("forceleave")) && sender.hasPermission(ParkourOption.ADMIN.permission)) { + completions.add("everyone"); + if (a1.equals("forcejoin")) completions.add("nearest"); + Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName())); + } else if (a1.equals("recoverinventory") && sender.hasPermission(ParkourOption.ADMIN.permission)) { + Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName())); + } + return filter(args[1], completions); + } + + return Collections.emptyList(); + } + + // ── util ─────────────────────────────────────────────────────────────────── + + private List filter(String input, List options) { + String lower = input.toLowerCase(); + return options.stream().filter(s -> s.toLowerCase().startsWith(lower)).toList(); + } + + private void send(CommandSender sender, String msg) { + sender.sendMessage(ColorUtil.color(msg)); } } diff --git a/src/main/java/dev/loki/loparkour/api/Registry.java b/src/main/java/dev/loki/loparkour/api/Registry.java index 1461459..81e537d 100644 --- a/src/main/java/dev/loki/loparkour/api/Registry.java +++ b/src/main/java/dev/loki/loparkour/api/Registry.java @@ -27,14 +27,10 @@ public final class Registry { */ public static void register(@NotNull Mode mode) { modes.add(mode); - - LoParkour.log("Registered mode %s".formatted(mode.getName())); } public static void register(@NotNull Style style) { styles.add(style); - - LoParkour.log("Registered style %s".formatted(style.getName())); } /** diff --git a/src/main/java/dev/loki/loparkour/command/AdminCommandHandler.java b/src/main/java/dev/loki/loparkour/command/AdminCommandHandler.java new file mode 100644 index 0000000..c159031 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/command/AdminCommandHandler.java @@ -0,0 +1,162 @@ +package dev.loki.loparkour.command; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.api.Registry; +import dev.loki.loparkour.config.Locales; +import dev.loki.loparkour.leaderboard.Leaderboard; +import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.mode.Mode; +import dev.loki.loparkour.mode.Modes; +import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.player.data.InventoryData; +import dev.loki.loparkour.util.ColorUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Handles admin-only /LoParkour sub-commands: + * forcejoin, forceleave, reset, recoverinventory. + */ +public class AdminCommandHandler { + + private final PlayerCommandHandler base; + + public AdminCommandHandler(PlayerCommandHandler base) { + this.base = base; + } + + public void handle(@NotNull String arg1, @NotNull String arg2, + @NotNull CommandSender sender, @Nullable Player player) { + + if (!sender.hasPermission(ParkourOption.ADMIN.permission)) return; + + switch (arg1.toLowerCase()) { + case "forcejoin" -> handleForceJoin(arg2, sender); + case "forceleave" -> handleForceLeave(arg2, sender); + case "reset" -> handleReset(arg2, sender); + case "recoverinventory" -> handleRecoverInventory(arg2, sender); + } + + if (player == null) return; + + if (arg1.equals("schematic")) { + SchematicCommandHandler.handleSubcommand(arg2, sender, player, base); + } + } + + // ── forcejoin ────────────────────────────────────────────────────────────── + + private void handleForceJoin(String target, CommandSender sender) { + if (target.equalsIgnoreCase("everyone")) { + Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p)); + send(sender, LoParkour.PREFIX + "Force joined everyone!"); + return; + } + if (target.equalsIgnoreCase("nearest")) { + Player closest = findNearest(sender); + if (closest == null) return; + Modes.DEFAULT.create(closest); + send(sender, LoParkour.PREFIX + "Force joined " + closest.getName() + "!"); + return; + } + Player other = Bukkit.getPlayer(target); + if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; } + Modes.DEFAULT.create(other); + } + + // ── forceleave ───────────────────────────────────────────────────────────── + + private void handleForceLeave(String target, CommandSender sender) { + if (target.equalsIgnoreCase("everyone")) { + ParkourPlayer.getPlayers().forEach(ParkourUser::leave); + send(sender, LoParkour.PREFIX + "Force kicked everyone!"); + return; + } + Player other = Bukkit.getPlayer(target); + if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; } + ParkourUser user = ParkourUser.getUser(other); + if (user == null) { send(sender, LoParkour.PREFIX + "Player isn't playing!"); return; } + ParkourUser.leave(user); + } + + // ── reset ────────────────────────────────────────────────────────────────── + + private void handleReset(String target, CommandSender sender) { + if (!base.cooldown(sender, "reset", 2500)) return; + + if (target.equalsIgnoreCase("everyone")) { + Registry.getModes().stream() + .map(Mode::getLeaderboard) + .filter(lb -> lb != null) + .forEach(lb -> { lb.resetAll(); lb.write(true); }); + send(sender, LoParkour.PREFIX + "Reset all high scores."); + return; + } + + UUID uuid = resolveUUID(target); + String name = resolvePlayerName(target, uuid); + + Registry.getModes().stream() + .map(Mode::getLeaderboard) + .filter(lb -> lb != null) + .forEach(lb -> { lb.remove(uuid); lb.write(true); }); + + send(sender, LoParkour.PREFIX + "Reset high score of " + name + "."); + } + + // ── recoverinventory ─────────────────────────────────────────────────────── + + private void handleRecoverInventory(String target, CommandSender sender) { + if (!base.cooldown(sender, "recoverinventory", 2500)) return; + Player other = Bukkit.getPlayer(target); + if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; } + + new InventoryData(other).load(result -> { + if (result != null) { + send(sender, LoParkour.PREFIX + "Recovered inventory of " + other.getName() + "."); + } else { + send(sender, LoParkour.PREFIX + "No saved inventory found for " + other.getName() + "."); + } + }); + } + + // ── helpers ──────────────────────────────────────────────────────────────── + + private @Nullable Player findNearest(CommandSender sender) { + Location from = sender instanceof Player p ? p.getLocation() + : sender instanceof BlockCommandSender b ? b.getBlock().getLocation() + : null; + if (from == null || from.getWorld() == null) return null; + + return from.getWorld().getPlayers().stream() + .min((a, b) -> Double.compare(a.getLocation().distance(from), b.getLocation().distance(from))) + .orElse(null); + } + + private UUID resolveUUID(String input) { + Player online = Bukkit.getPlayerExact(input); + if (online != null) return online.getUniqueId(); + if (input.contains("-")) return UUID.fromString(input); + return Bukkit.getOfflinePlayer(input).getUniqueId(); + } + + private String resolvePlayerName(String input, UUID uuid) { + Player online = Bukkit.getPlayerExact(input); + if (online != null) return online.getName(); + OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); + return op.getName() != null ? op.getName() : input; + } + + private void send(CommandSender sender, String msg) { + sender.sendMessage(ColorUtil.color(msg)); + } +} diff --git a/src/main/java/dev/loki/loparkour/command/PlayerCommandHandler.java b/src/main/java/dev/loki/loparkour/command/PlayerCommandHandler.java new file mode 100644 index 0000000..70f5877 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/command/PlayerCommandHandler.java @@ -0,0 +1,121 @@ +package dev.loki.loparkour.command; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.Locales; +import dev.loki.loparkour.menu.Menus; +import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.mode.Modes; +import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.util.ColorUtil; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * Handles all player-facing /parkour sub-commands. + * Admin commands are in {@link AdminCommandHandler}. + */ +public class PlayerCommandHandler { + + private final Map cooldowns = new HashMap<>(); + + public void handleNoArgs(@NotNull CommandSender sender, @Nullable Player player) { + if (player != null && ParkourOption.MAIN.mayPerform(player)) { + Menus.MAIN.open(player); + } else { + sendHelp(sender); + } + } + + public void handle(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) { + switch (arg.toLowerCase()) { + case "help" -> sendHelp(sender); + case "reload" -> handleReload(sender, player); + } + + if (player == null) return; + + switch (arg.toLowerCase()) { + case "join" -> { + if (!cooldown(sender, "join", 2500)) return; + if (!ParkourOption.JOIN.mayPerform(player)) { + send(sender, Locales.getString(player, "other.no_do")); + return; + } + if (ParkourUser.getUser(player) != null) return; + Modes.DEFAULT.create(player); + } + case "leave" -> { + if (!cooldown(sender, "leave", 2500)) return; + ParkourUser.leave(player); + } + case "play" -> { + if (ParkourOption.PLAY.mayPerform(player)) Menus.PLAY.open(player); + } + case "menu", "main" -> { + if (ParkourOption.MAIN.mayPerform(player)) Menus.MAIN.open(player); + } + case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid"); + case "schematic" -> { + if (!player.hasPermission(ParkourOption.ADMIN.permission)) { + send(sender, Locales.getString(player, "other.no_do")); + return; + } + SchematicCommandHandler.sendHelp(player); + } + } + } + + private void handleReload(@NotNull CommandSender sender, @Nullable Player player) { + if (!cooldown(sender, "reload", 2500)) return; + if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { + send(sender, Locales.getString(player, "other.no_do")); + return; + } + Config.reload(false); + send(sender, LoParkour.PREFIX + "Reloaded config files."); + } + + public void sendHelp(@NotNull CommandSender sender) { + send(sender, ""); + send(sender, "--------------- " + LoParkour.NAME + " ---------------"); + send(sender, ""); + send(sender, "/parkour - Main command"); + if (sender.hasPermission(ParkourOption.JOIN.permission)) { + send(sender, "/parkour join [mode/player] - Join a mode"); + send(sender, "/parkour leave - Leave parkour"); + } + if (sender.hasPermission(ParkourOption.MAIN.permission)) + send(sender, "/parkour menu - Open the menu"); + if (sender.hasPermission(ParkourOption.PLAY.permission)) + send(sender, "/parkour play - Mode selection menu"); + if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) + send(sender, "/parkour leaderboard [type] - Leaderboard"); + if (sender.hasPermission(ParkourOption.ADMIN.permission)) { + send(sender, "/LoParkour schematic - Schematic tools"); + send(sender, "/LoParkour reload - Reload configs"); + send(sender, "/LoParkour reset - Reset scores"); + send(sender, "/LoParkour forcejoin/forceleave - Force join/leave"); + send(sender, "/LoParkour recoverinventory - Recover inventory"); + } + send(sender, ""); + } + + public boolean cooldown(CommandSender sender, String key, long millis) { + String fullKey = sender.getName() + ":" + key; + long now = System.currentTimeMillis(); + Long last = cooldowns.get(fullKey); + if (last != null && now - last < millis) return false; + cooldowns.put(fullKey, now); + return true; + } + + private void send(CommandSender sender, String message) { + sender.sendMessage(ColorUtil.color(message)); + } +} diff --git a/src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java b/src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java new file mode 100644 index 0000000..e454486 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java @@ -0,0 +1,149 @@ +package dev.loki.loparkour.command; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.schematic.lpschem.LPSchematic; +import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder; +import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.util.Locations; +import dev.loki.loparkour.util.ParticleUtil; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.Material; +import org.bukkit.util.BoundingBox; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Handles /LoParkour schematic sub-commands (wand, pos1, pos2, save, paste, list, reload). + */ +public class SchematicCommandHandler { + + public static final Map selections = new HashMap<>(); + + private static final ItemStack WAND = buildWand(); + + private static ItemStack buildWand() { + ItemStack item = new ItemStack(Material.GOLDEN_AXE); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(ColorUtil.color("LPSchematic Wand")); + meta.setLore(List.of( + ColorUtil.color("Left click: first position"), + ColorUtil.color("Right click: second position") + )); + item.setItemMeta(meta); + } + return item; + } + + public static ItemStack getWand() { + return WAND.clone(); + } + + public static void sendHelp(Player player) { + send(player, ""); + send(player, "Schematics -----------"); + send(player, "/LoParkour schematic wand - Get the wand"); + send(player, "/LoParkour schematic pos1 - Set position 1"); + send(player, "/LoParkour schematic pos2 - Set position 2"); + send(player, "/LoParkour schematic save - Save selection"); + send(player, "/LoParkour schematic paste - Paste schematic"); + send(player, "/LoParkour schematic list - List loaded schematics"); + send(player, "/LoParkour schematic reload - Reload schematics"); + send(player, ""); + } + + /** Called for 2-arg schematic commands (wand/pos1/pos2/list/reload) */ + public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) { + Location playerLoc = player.getLocation(); + Location[] sel = selections.get(player); + + switch (sub.toLowerCase()) { + case "wand" -> { + player.getInventory().addItem(WAND.clone()); + send(player, "Schematics Wand given. Left/Right click to set positions."); + } + case "pos1" -> setPos(player, sel, playerLoc, 0); + case "pos2" -> setPos(player, sel, playerLoc, 1); + case "list" -> { + send(player, "Schematics Loaded schematics:"); + LoParkour.getSchematicManager().getAllSchematics().forEach((name, s) -> + send(player, "- " + name + " (" + s.getMetadata().getDifficulty() + ")")); + } + case "reload" -> { + if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) return; + LoParkour.getSchematicManager().reload(); + send(player, "Schematics Reloaded."); + } + } + } + + /** Called for 3-arg schematic commands (save , paste ) */ + public static void handleSubcommandWithName(String sub, String name, CommandSender sender, + Player player, PlayerCommandHandler cooldowns) { + switch (sub.toLowerCase()) { + case "save" -> handleSave(name, sender, player, cooldowns); + case "paste" -> handlePaste(name, sender, player); + } + } + + // ── internal ─────────────────────────────────────────────────────────────── + + private static void setPos(Player player, Location[] existing, Location loc, int index) { + String label = index == 0 ? "1" : "2"; + send(player, LoParkour.PREFIX + "Position " + label + " set to " + Locations.toString(loc, true)); + + Location[] updated = existing != null ? existing.clone() : new Location[]{null, null}; + updated[index] = loc; + selections.put(player, updated); + + if (updated[0] != null && updated[1] != null) { + ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2); + } + } + + private static void handleSave(String name, CommandSender sender, Player player, PlayerCommandHandler cooldowns) { + if (!cooldowns.cooldown(sender, "schematic-save", 2500)) return; + + Location[] sel = selections.get(player); + if (sel == null || sel[0] == null || sel[1] == null) { + send(player, "Schematics Selection incomplete. Set both positions first."); + return; + } + + try { + int w = Math.abs(sel[1].getBlockX() - sel[0].getBlockX()) + 1; + int h = Math.abs(sel[1].getBlockY() - sel[0].getBlockY()) + 1; + int l = Math.abs(sel[1].getBlockZ() - sel[0].getBlockZ()) + 1; + + LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), 0.5, w, h, l) + .fromWorld(player.getWorld(), sel[0], sel[1]) + .build(); + + LoParkour.getSchematicManager().saveSchematic(schematic); + send(player, "Schematics Saved as " + name + "!"); + } catch (Exception e) { + send(player, "Schematics Failed: " + e.getMessage()); + } + } + + private static void handlePaste(String name, CommandSender sender, Player player) { + LPSchematic s = LoParkour.getSchematicManager().getSchematic(name); + if (s == null) { + send(sender, LoParkour.PREFIX + "Schematic '" + name + "' not found."); + return; + } + s.paste(player.getLocation(), player.getWorld()); + send(sender, LoParkour.PREFIX + "Pasted " + name + "."); + } + + private static void send(CommandSender sender, String msg) { + sender.sendMessage(ColorUtil.color(msg)); + } +} diff --git a/src/main/java/dev/loki/loparkour/config/Config.java b/src/main/java/dev/loki/loparkour/config/Config.java index 5a731b5..1609077 100644 --- a/src/main/java/dev/loki/loparkour/config/Config.java +++ b/src/main/java/dev/loki/loparkour/config/Config.java @@ -75,8 +75,6 @@ public enum Config { Locales.init(); Schematics.init(); Option.init(initialLoad); - - LoParkour.log("Loaded all config files"); } /** diff --git a/src/main/java/dev/loki/loparkour/config/GenerationOptions.java b/src/main/java/dev/loki/loparkour/config/GenerationOptions.java new file mode 100644 index 0000000..b58edc4 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/config/GenerationOptions.java @@ -0,0 +1,115 @@ +package dev.loki.loparkour.config; + +import dev.loki.loparkour.LoParkour; + +/** + * Generation and jump-validation settings. Populated by {@link Option#init}. + */ +public final class GenerationOptions { + + // Block type chances + public static double TYPE_NORMAL; + public static double TYPE_SPECIAL; + public static double TYPE_SCHEMATICS; + + // Special block chances + public static double SPECIAL_ICE; + public static double SPECIAL_SLAB; + public static double SPECIAL_PANE; + public static double SPECIAL_FENCE; + + // Distance weights + public static double NORMAL_DISTANCE_1; + public static double NORMAL_DISTANCE_2; + public static double NORMAL_DISTANCE_3; + public static double NORMAL_DISTANCE_4; + + // Height weights + public static double NORMAL_HEIGHT_1; + public static double NORMAL_HEIGHT_0; + public static double NORMAL_HEIGHT_NEG1; + public static double NORMAL_HEIGHT_NEG2; + + // World bounds + public static int MAX_Y; + public static int MIN_Y; + + // Jump validation + public static boolean JUMP_VALIDATION_ENABLED; + public static double MAX_JUMP_DISTANCE; + public static double MAX_HORIZONTAL_DISTANCE; + public static double MAX_VERTICAL_UP; + public static double MAX_VERTICAL_DOWN; + + // Jump types + public static boolean JUMP_TYPES_ENABLED; + public static java.util.Map JUMP_TYPE_ENABLED; + public static java.util.Map JUMP_TYPE_CHANCE; + + // Memory optimisation + public static int BLOCK_CLEANUP_DISTANCE; + public static int CLEANUP_INTERVAL; + + // Ghost mode + public static boolean GHOST_MODE_ENABLED; + public static int GHOST_SHOW_TOP; + public static double GHOST_TRANSPARENCY; + + static void init() { + // Jump validation + JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled"); + MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance"); + MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal"); + MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up"); + MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down"); + + // Jump types + JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled"); + JUMP_TYPE_ENABLED = new java.util.HashMap<>(); + JUMP_TYPE_CHANCE = new java.util.HashMap<>(); + for (String type : Config.CONFIG.getChildren("jump-types.types")) { + String path = "jump-types.types." + type; + JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled")); + JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance")); + } + + // Memory + BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance"); + CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval"); + + // Ghost + GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled"); + GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top"); + GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency"); + + // Generation chances + TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0; + TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.special") / 100.0; + TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.schematic") / 100.0; + + SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0; + SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0; + SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0; + SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0; + + NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0; + NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0; + NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0; + NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0; + + NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0; + NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0; + NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0; + NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0; + + MAX_Y = Config.GENERATION.getInt("generation.settings.max-y"); + MIN_Y = Config.GENERATION.getInt("generation.settings.min-y"); + + if (MIN_Y >= MAX_Y) { + MIN_Y = 100; MAX_Y = 200; + LoParkour.getPlugin().getLogger().severe("min-y >= max-y in generation.yml — using defaults 100/200."); + } + } + + private GenerationOptions() {} +} diff --git a/src/main/java/dev/loki/loparkour/config/Locales.java b/src/main/java/dev/loki/loparkour/config/Locales.java index 18a5aa5..2efb153 100644 --- a/src/main/java/dev/loki/loparkour/config/Locales.java +++ b/src/main/java/dev/loki/loparkour/config/Locales.java @@ -69,9 +69,6 @@ public class Locales { if (files != null && files.length == 0) { plugin.saveResource("locales/en.yml", false); plugin.saveResource("locales/ru.yml", false); - plugin.saveResource("locales/nl.yml", false); - plugin.saveResource("locales/fr.yml", false); - plugin.saveResource("locales/zh_cn.yml", false); } // get all files in locales folder @@ -241,7 +238,13 @@ public class Locales { idx++; } - Item item = new Item(Material.getMaterial(material.toUpperCase()), name); + Material mat = Material.getMaterial(material.toUpperCase()); + if (mat == null) { + LoParkour.getPlugin().getLogger().warning("Invalid material '%s' for locale path '%s', using STONE as fallback".formatted(material, path)); + mat = Material.STONE; + } + + Item item = new Item(mat, name); if (!lore.isEmpty()) { item.lore(lore.split("\\|\\|")); diff --git a/src/main/java/dev/loki/loparkour/config/Option.java b/src/main/java/dev/loki/loparkour/config/Option.java index 685cd7e..f629772 100644 --- a/src/main/java/dev/loki/loparkour/config/Option.java +++ b/src/main/java/dev/loki/loparkour/config/Option.java @@ -1,17 +1,11 @@ package dev.loki.loparkour.config; -import org.bukkit.Bukkit; - -import java.util.ArrayList; - -import dev.loki.loparkour.util.ParticleData; - import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.api.Registry; import dev.loki.loparkour.menu.ParkourOption; import dev.loki.loparkour.style.RandomStyle; import dev.loki.loparkour.style.Style; - +import dev.loki.loparkour.util.ParticleData; import org.bukkit.*; import org.bukkit.block.BlockFace; import org.bukkit.configuration.file.FileConfiguration; @@ -21,303 +15,198 @@ import java.util.*; import java.util.function.BiFunction; /** - * Class for variables required in generating without accessing the file a lot (constants) + * Top-level config constants used throughout the plugin. + * + * Heavy sub-sections are delegated to focused classes: + *
    + *
  • {@link SqlOptions} – SQL connection settings
  • + *
  • {@link ParticleOptions} – particle & sound settings
  • + *
  • {@link GenerationOptions} – generation & jump-validation settings
  • + *
+ * + * Those classes expose their own static fields. Option re-exports the most + * commonly accessed ones via forwarding fields so existing call-sites compile + * without changes. */ public class Option { + // ── General ─────────────────────────────────────────────────────────────── + public static double BORDER_SIZE; public static List POSSIBLE_LEADS; - - // Advanced settings public static BlockFace HEADING; public static Map OPTIONS_ENABLED; - public static Map OPTIONS_DEFAULTS; + public static Map OPTIONS_DEFAULTS; public static Location GO_BACK_LOC; - public static void init(boolean firstLoad) { - initSql(); - initEnums(); - initGeneration(); - initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new) - .forEach(Registry::register); + // ── Forwarded from sub-classes (backwards compat) ───────────────────────── + /** @see SqlOptions#SQL */ + public static boolean SQL; + + /** @see ParticleOptions#SHAPE */ + public static ParticleShape PARTICLE_SHAPE; + /** @see ParticleOptions#SOUND_TYPE */ + public static Sound SOUND_TYPE; + /** @see ParticleOptions#SOUND_PITCH */ + public static int SOUND_PITCH; + /** @see ParticleOptions#SOUND_VOLUME */ + public static int SOUND_VOLUME; + /** @see ParticleOptions#PARTICLE_TYPE */ + public static Particle PARTICLE_TYPE; + /** @see ParticleOptions#PARTICLE_DATA */ + public static ParticleData PARTICLE_DATA; + + /** @see GenerationOptions */ + public static double TYPE_NORMAL, TYPE_SPECIAL, TYPE_SCHEMATICS; + public static double SPECIAL_ICE, SPECIAL_SLAB, SPECIAL_PANE, SPECIAL_FENCE; + public static double NORMAL_DISTANCE_1, NORMAL_DISTANCE_2, NORMAL_DISTANCE_3, NORMAL_DISTANCE_4; + public static double NORMAL_HEIGHT_1, NORMAL_HEIGHT_0, NORMAL_HEIGHT_NEG1, NORMAL_HEIGHT_NEG2; + public static int MAX_Y, MIN_Y; + public static int BLOCK_CLEANUP_DISTANCE, CLEANUP_INTERVAL; + public static boolean GHOST_MODE_ENABLED; + public static int GHOST_SHOW_TOP; + public static double GHOST_TRANSPARENCY; + + // ── init ────────────────────────────────────────────────────────────────── + + public static void init(boolean firstLoad) { + SqlOptions.init(); + ParticleOptions.init(); + GenerationOptions.init(); + + syncForwardedFields(); + initGeneral(firstLoad); + initOptions(); + + initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new) + .forEach(Registry::register); + } + + /** Copy sub-class fields to this class so existing code still compiles. */ + private static void syncForwardedFields() { + SQL = SqlOptions.SQL; + + PARTICLE_SHAPE = ParticleOptions.SHAPE; + SOUND_TYPE = ParticleOptions.SOUND_TYPE; + SOUND_PITCH = ParticleOptions.SOUND_PITCH; + SOUND_VOLUME = ParticleOptions.SOUND_VOLUME; + PARTICLE_TYPE = ParticleOptions.PARTICLE_TYPE; + PARTICLE_DATA = ParticleOptions.PARTICLE_DATA; + + TYPE_NORMAL = GenerationOptions.TYPE_NORMAL; + TYPE_SPECIAL = GenerationOptions.TYPE_SPECIAL; + TYPE_SCHEMATICS = GenerationOptions.TYPE_SCHEMATICS; + + SPECIAL_ICE = GenerationOptions.SPECIAL_ICE; + SPECIAL_SLAB = GenerationOptions.SPECIAL_SLAB; + SPECIAL_PANE = GenerationOptions.SPECIAL_PANE; + SPECIAL_FENCE = GenerationOptions.SPECIAL_FENCE; + + NORMAL_DISTANCE_1 = GenerationOptions.NORMAL_DISTANCE_1; + NORMAL_DISTANCE_2 = GenerationOptions.NORMAL_DISTANCE_2; + NORMAL_DISTANCE_3 = GenerationOptions.NORMAL_DISTANCE_3; + NORMAL_DISTANCE_4 = GenerationOptions.NORMAL_DISTANCE_4; + + NORMAL_HEIGHT_1 = GenerationOptions.NORMAL_HEIGHT_1; + NORMAL_HEIGHT_0 = GenerationOptions.NORMAL_HEIGHT_0; + NORMAL_HEIGHT_NEG1 = GenerationOptions.NORMAL_HEIGHT_NEG1; + NORMAL_HEIGHT_NEG2 = GenerationOptions.NORMAL_HEIGHT_NEG2; + + MAX_Y = GenerationOptions.MAX_Y; + MIN_Y = GenerationOptions.MIN_Y; + + BLOCK_CLEANUP_DISTANCE = GenerationOptions.BLOCK_CLEANUP_DISTANCE; + CLEANUP_INTERVAL = GenerationOptions.CLEANUP_INTERVAL; + + GHOST_MODE_ENABLED = GenerationOptions.GHOST_MODE_ENABLED; + GHOST_SHOW_TOP = GenerationOptions.GHOST_SHOW_TOP; + GHOST_TRANSPARENCY = GenerationOptions.GHOST_TRANSPARENCY; + } + + // ── General init ────────────────────────────────────────────────────────── + + private static void initGeneral(boolean firstLoad) { GO_BACK_LOC = parseLocation(Config.CONFIG.getString("bungeecord.go-back")); String[] axes = Config.CONFIG.getString("bungeecord.go-back-axes").split(","); GO_BACK_LOC.setPitch(Float.parseFloat(axes[0])); GO_BACK_LOC.setYaw(Float.parseFloat(axes[1])); - // General settings - - // Options - - List options = new ArrayList<>(Arrays.asList(ParkourOption.values())); - - // exceptions - options.remove(ParkourOption.JOIN); - options.remove(ParkourOption.ADMIN); - - // ===================================== - - OPTIONS_DEFAULTS = new HashMap<>(); - OPTIONS_ENABLED = new HashMap<>(); - - String prefix = "default-values"; - for (ParkourOption option : options) { - String parent = "%s.%s".formatted(prefix, option.path); - - // register enabled value - OPTIONS_ENABLED.put(option, Config.CONFIG.getBoolean("%s.enabled".formatted(parent))); - - // register default value - if (!Config.CONFIG.isPath("%s.default".formatted(parent))) { - continue; - } - - Object value = Config.CONFIG.get("%s.default".formatted(parent)); - - if (value != null) { - OPTIONS_DEFAULTS.put(option, String.valueOf(value)); - } - } - - // ===================================== - - // Config stuff - POSSIBLE_LEADS = Config.CONFIG.getIntList("options.leads.amount"); for (int lead : new ArrayList<>(POSSIBLE_LEADS)) { if (lead < 1 || lead > 128) { - LoParkour.getPlugin().getLogger().severe("Invalid lead: %d. Should be above 1 and below 128.".formatted(lead)); + LoParkour.getPlugin().getLogger().severe("Invalid lead: %d. Must be 1–128.".formatted(lead)); POSSIBLE_LEADS.remove((Object) lead); } } - // Generation String heading = Config.GENERATION.getString("advanced.island.parkour.heading"); - - switch (heading.toLowerCase()) { - case "north" -> HEADING = BlockFace.NORTH; - case "south" -> HEADING = BlockFace.SOUTH; - case "west" -> HEADING = BlockFace.WEST; - case "east" -> HEADING = BlockFace.EAST; - default -> LoParkour.getPlugin().getLogger().severe("Invalid heading: %s".formatted(heading)); - } - - // Scoring + HEADING = switch (heading.toLowerCase()) { + case "north" -> BlockFace.NORTH; + case "south" -> BlockFace.SOUTH; + case "west" -> BlockFace.WEST; + case "east" -> BlockFace.EAST; + default -> { + LoParkour.getPlugin().getLogger().severe("Invalid heading: " + heading); + yield BlockFace.NORTH; + } + }; if (firstLoad) { BORDER_SIZE = Config.GENERATION.getDouble("advanced.border-size"); - SQL = Config.CONFIG.getBoolean("sql.enabled"); } } - private static Vector stringToVector(String direction) { - return switch (direction.toLowerCase()) { - case "north" -> new Vector(0, 0, -1); - case "south" -> new Vector(0, 0, 1); - case "west" -> new Vector(-1, 0, 0); - default -> new Vector(1, 0, 0); // east - }; + private static void initOptions() { + List options = new ArrayList<>(Arrays.asList(ParkourOption.values())); + options.remove(ParkourOption.JOIN); + options.remove(ParkourOption.ADMIN); + + OPTIONS_DEFAULTS = new HashMap<>(); + OPTIONS_ENABLED = new HashMap<>(); + + for (ParkourOption option : options) { + String parent = "default-values." + option.path; + OPTIONS_ENABLED.put(option, Config.CONFIG.getBoolean(parent + ".enabled")); + + if (Config.CONFIG.isPath(parent + ".default")) { + Object value = Config.CONFIG.get(parent + ".default"); + if (value != null) OPTIONS_DEFAULTS.put(option, String.valueOf(value)); + } + } } + // ── Helpers ─────────────────────────────────────────────────────────────── + private static Location parseLocation(String location) { String[] values = location.replaceAll("[()]", "").replaceAll("[, ]", " ").split(" "); - World world = Bukkit.getWorld(values[3]); - - if (world == null) { - world = Bukkit.getWorlds().get(0); - } - - return new Location(world, Double.parseDouble(values[0]), Double.parseDouble(values[1]), Double.parseDouble(values[2])); + if (world == null) world = Bukkit.getWorlds().get(0); + return new Location(world, Double.parseDouble(values[0]), + Double.parseDouble(values[1]), Double.parseDouble(values[2])); } - public static ParticleShape PARTICLE_SHAPE; - public static Sound SOUND_TYPE; - public static int SOUND_PITCH; - public static int SOUND_VOLUME; - public static Particle PARTICLE_TYPE; - public static ParticleData PARTICLE_DATA; - - private static void initEnums() { - String value = Config.CONFIG.getString("particles.sound-type").toUpperCase(); - - try { - SOUND_TYPE = Sound.valueOf(value); - } catch (IllegalArgumentException ex) { - SOUND_TYPE = Sound.valueOf("BLOCK_NOTE_PLING"); - LoParkour.getPlugin().getLogger().severe("Invalid sound: %s".formatted(value)); - } - - value = Config.CONFIG.getString("particles.particle-type"); - try { - PARTICLE_TYPE = Particle.valueOf(value); - } catch (IllegalArgumentException ex) { - PARTICLE_TYPE = Particle.valueOf("SPELL_INSTANT"); - LoParkour.getPlugin().getLogger().severe("Invalid particle type: %s".formatted(value)); - } - - SOUND_PITCH = Config.CONFIG.getInt("particles.sound-pitch"); - SOUND_VOLUME = Config.CONFIG.getInt("particles.sound-volume"); - PARTICLE_SHAPE = ParticleShape.valueOf(Config.CONFIG.getString("particles.particle-shape").toUpperCase()); - PARTICLE_DATA = new ParticleData<>(PARTICLE_TYPE, null, 10); - } - - public enum ParticleShape { - DOT, CIRCLE, BOX - } - - // -------------------------------------------------------------- - // MySQL - public static boolean SQL; - public static int SQL_PORT; - public static String SQL_URL; - public static String SQL_DB; - public static String SQL_USERNAME; - public static String SQL_PASSWORD; - public static String SQL_PREFIX; - - private static void initSql() { - SQL_PORT = Config.CONFIG.getInt("sql.port"); - SQL_DB = Config.CONFIG.getString("sql.database"); - SQL_URL = Config.CONFIG.getString("sql.url"); - SQL_USERNAME = Config.CONFIG.getString("sql.username"); - SQL_PASSWORD = Config.CONFIG.getString("sql.password"); - SQL_PREFIX = Config.CONFIG.getString("sql.prefix"); - } - - // -------------------------------------------------------------- - // Jump Validation - - public static boolean JUMP_VALIDATION_ENABLED; - public static double MAX_JUMP_DISTANCE; - public static double MAX_HORIZONTAL_DISTANCE; - public static double MAX_VERTICAL_UP; - public static double MAX_VERTICAL_DOWN; - - // Jump Types - - public static boolean JUMP_TYPES_ENABLED; - public static Map JUMP_TYPE_ENABLED; - public static Map JUMP_TYPE_CHANCE; - - // Memory Optimization - - public static int BLOCK_CLEANUP_DISTANCE; - public static int CLEANUP_INTERVAL; - - // Ghost Mode - - public static boolean GHOST_MODE_ENABLED; - public static int GHOST_SHOW_TOP; - public static double GHOST_TRANSPARENCY; - - // -------------------------------------------------------------- - // Generation - - public static double TYPE_NORMAL; - public static double TYPE_SPECIAL; - public static double TYPE_SCHEMATICS; - - public static double SPECIAL_ICE; - public static double SPECIAL_SLAB; - public static double SPECIAL_PANE; - public static double SPECIAL_FENCE; - - public static double NORMAL_DISTANCE_1; - public static double NORMAL_DISTANCE_2; - public static double NORMAL_DISTANCE_3; - public static double NORMAL_DISTANCE_4; - - public static double NORMAL_HEIGHT_1; - public static double NORMAL_HEIGHT_0; - public static double NORMAL_HEIGHT_NEG1; - public static double NORMAL_HEIGHT_NEG2; - - public static int MAX_Y; - public static int MIN_Y; - - private static void initGeneration() { - // Jump validation - JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled"); - MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance"); - MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal"); - MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up"); - MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down"); - - // Jump types - JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled"); - JUMP_TYPE_ENABLED = new HashMap<>(); - JUMP_TYPE_CHANCE = new HashMap<>(); - - for (String type : Config.CONFIG.getChildren("jump-types.types")) { - String path = "jump-types.types." + type; - JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled")); - JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance")); - } - - // Memory optimization - BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance"); - CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval"); - - // Ghost mode - GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled"); - GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top"); - GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency"); - - TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0; - TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.schematic") / 100.0; - TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.special") / 100.0; - - SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0; - SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0; - SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0; - SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0; - - NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0; - NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0; - NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0; - NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0; - - NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0; - NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0; - NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0; - NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0; - - MAX_Y = Config.GENERATION.getInt("generation.settings.max-y"); - MIN_Y = Config.GENERATION.getInt("generation.settings.min-y"); - - if (MIN_Y >= MAX_Y) { - MIN_Y = 100; - MAX_Y = 200; - - LoParkour.getPlugin().getLogger().severe("Provided minimum y is the same or larger than maximum y! - check your generation.yml file"); - } - } - - // -------------------------------------------------------------- - public static Set