fix all migrate 80%
This commit is contained in:
parent
d1dcf6ab57
commit
574f9fc4b5
21 changed files with 1054 additions and 1154 deletions
91
REFACTORING_SUMMARY.md
Normal file
91
REFACTORING_SUMMARY.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Рефакторинг - Краткое описание
|
||||||
|
|
||||||
|
## Выполненные изменения
|
||||||
|
|
||||||
|
### 1. StorageSQL.java (248 → ~150 строк)
|
||||||
|
|
||||||
|
**Проблемы:**
|
||||||
|
- Смешивание логики подключения, миграций и CRUD операций
|
||||||
|
- Дублирование кода в методах sendUpdate/sendUpdateSuppressed
|
||||||
|
- Отсутствие разделения ответственностей
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
Разделен на 3 класса:
|
||||||
|
- `SQLConnectionManager` - управление подключением к БД
|
||||||
|
- `SQLQueryExecutor` - выполнение SQL запросов
|
||||||
|
- `SQLMigrationManager` - управление миграциями схемы БД
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- Каждый класс отвечает за одну задачу (Single Responsibility Principle)
|
||||||
|
- Легче тестировать и поддерживать
|
||||||
|
- Упрощена логика переподключения
|
||||||
|
|
||||||
|
### 2. Session.java (236 → ~120 строк)
|
||||||
|
|
||||||
|
**Проблемы:**
|
||||||
|
- Слишком много ответственностей (игроки, спектаторы, видимость, мут)
|
||||||
|
- Дублирование логики в add/remove методах
|
||||||
|
- Сложная логика уведомлений
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
Создан класс `SessionUserManager` для управления пользователями
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- Session теперь делегирует управление пользователями
|
||||||
|
- Логика уведомлений инкапсулирована
|
||||||
|
- Проще добавлять новые типы пользователей
|
||||||
|
|
||||||
|
### 3. ParkourUser.java (220 → ~100 строк)
|
||||||
|
|
||||||
|
**Проблемы:**
|
||||||
|
- Смешивание статических методов регистрации и instance методов
|
||||||
|
- Сложная логика scoreboard встроена в класс
|
||||||
|
- Запутанные методы register/unregister/leave
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
Разделен на 3 класса:
|
||||||
|
- `UserRegistry` - регистрация и управление пользователями
|
||||||
|
- `ScoreboardManager` - управление scoreboard
|
||||||
|
- `BungeeUtil` - утилиты для BungeeCord
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- Четкое разделение статической и instance логики
|
||||||
|
- Scoreboard логика изолирована и переиспользуема
|
||||||
|
- Упрощена логика регистрации/выхода
|
||||||
|
|
||||||
|
### 4. ParkourPlayer.java (202 → ~150 строк)
|
||||||
|
|
||||||
|
**Проблемы:**
|
||||||
|
- Огромная статическая инициализация PLAYER_COLUMNS
|
||||||
|
- Дублирование логики с ParkourUser
|
||||||
|
- Сложный метод setSettings
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
Создан класс `PlayerSettingsManager` для управления настройками
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- Настройки и их маппинг инкапсулированы
|
||||||
|
- Легче добавлять новые настройки
|
||||||
|
- Упрощена логика применения настроек
|
||||||
|
|
||||||
|
## Итоговая статистика
|
||||||
|
|
||||||
|
| Файл | Было строк | Стало строк | Новых классов |
|
||||||
|
|------|------------|-------------|---------------|
|
||||||
|
| StorageSQL.java | 248 | ~150 | 3 |
|
||||||
|
| Session.java | 236 | ~120 | 1 |
|
||||||
|
| ParkourUser.java | 220 | ~100 | 3 |
|
||||||
|
| ParkourPlayer.java | 202 | ~150 | 1 |
|
||||||
|
| **ИТОГО** | **906** | **~520** | **8** |
|
||||||
|
|
||||||
|
## Принципы, примененные в рефакторинге
|
||||||
|
|
||||||
|
1. **Single Responsibility Principle (SRP)** - каждый класс отвечает за одну задачу
|
||||||
|
2. **Separation of Concerns** - разделение логики по разным классам
|
||||||
|
3. **DRY (Don't Repeat Yourself)** - устранение дублирования кода
|
||||||
|
4. **Encapsulation** - инкапсуляция сложной логики в отдельные классы
|
||||||
|
5. **Delegation** - делегирование задач специализированным классам
|
||||||
|
|
||||||
|
## Обратная совместимость
|
||||||
|
|
||||||
|
Все публичные API остались без изменений. Рефакторинг затронул только внутреннюю структуру классов.
|
||||||
266
TODO.md
266
TODO.md
|
|
@ -1,248 +1,70 @@
|
||||||
# LoParkour — TODO & Roadmap
|
# LoParkour — TODO
|
||||||
|
|
||||||
> **Принципы:** KISS · DRY · SOLID
|
> Обновлено: 2026-03-07
|
||||||
> **Версия:** 1.3.0 → 2.0.0
|
|
||||||
> **Обновлено:** 2026-03-05
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Статус миграции vilib → LoLib 2.0
|
|
||||||
|
|
||||||
✅ **МИГРАЦИЯ ЗАВЕРШЕНА** (2026-03-01)
|
|
||||||
- Файлов изменено: 120+
|
|
||||||
- Строк кода: 3000+
|
|
||||||
- Ошибок исправлено: 250+
|
|
||||||
- Размер jar: 655 КБ
|
|
||||||
- Все меню работают
|
|
||||||
- Схематики работают
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Легенда
|
## Легенда
|
||||||
- 🔴 **Критично** — баги/краши, нужно прямо сейчас
|
- 🔴 Критично — баг/краш/не компилируется
|
||||||
- 🟡 **Важно** — неработающие фичи
|
- 🟡 Важно — фича заявлена но не работает
|
||||||
- 🟢 **Планово** — улучшения и новые фичи
|
- 🟢 Планово — улучшение
|
||||||
- ✅ **Готово**
|
- ✅ Готово
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔴 Критично
|
## 🔴 Критично
|
||||||
|
|
||||||
| # | Задача | Файл | Статус |
|
| # | Баг | Файл | Что сделать |
|
||||||
|---|--------|------|--------|
|
|---|-----|------|-------------|
|
||||||
| 1 | GUIManager.init() не вызывался — клики не отменялись | `LoParkour.java` | ✅ |
|
| 1 | `PotionEffectType.JUMP` и `SLOW` удалены в Paper 1.20.5+ | `GravityShiftMode.java` | Заменить на `JUMP_BOOST` и `SLOWNESS` |
|
||||||
| 2 | SQL race condition: CREATE TABLE до connect() | `StorageSQL.java` | ✅ |
|
| 2 | `ElytraMode` создаёт обычный `ParkourGenerator` → `getMode()` возвращает DEFAULT | `ElytraMode.java` | Создать `ElytraGenerator extends ParkourGenerator` |
|
||||||
| 3 | PreparedStatement leak в sendQuery() | `StorageSQL.java` | ✅ |
|
| 3 | `SpeedrunMode`: таймеры `runLater` продолжают тикать после `reset()` — гонка с генератором | `SpeedrunMode.java` | Хранить `ScheduledTask`, отменять в `reset()` |
|
||||||
| 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`
|
| 4 | `HardcoreMode`: `collectedRewards` не очищается при падении | `HardcoreMode.java` | Добавить `player.collectedRewards.clear()` в `fall()` |
|
||||||
- [ ] Протестировать все три режима в игре
|
| 5 | `JumpValidator` создан но нигде не вызывается | `BlockPlacer.java` | Добавить retry-loop в `selectNext()` (до 10 попыток) |
|
||||||
- [ ] Убедиться что `ParticleUtil.circle()` принимает `List<Player>`
|
| 6 | `JumpType` enum создан но не интегрирован в генератор | `BlockPlacer.java` | Добавить выбор JumpType по шансу из конфига |
|
||||||
|
| 7 | `ConfigUpdater` закомментирован | `Config.java` | Раскомментировать, проверить `ignoredSections` |
|
||||||
### ConfigUpdater
|
| 8 | `GravityShiftMode`: нет визуала при смене эффекта | `GravityShiftMode.java` | Добавить Title + Sound + партиклы `PORTAL` |
|
||||||
**Файл:** `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` — генерировать кольца из партиклов
|
| 9 | Ghost система не интегрирована | `ghost/` | Подключить `GhostRecorder` к `GeneratorLifecycle` |
|
||||||
- [ ] Детекция пролёта через кольцо (bounding box check в tick)
|
| 10 | Удалить мёртвые stub-файлы | `generator/`, корень | `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
|
||||||
- [ ] Cooldown на фейерверки (буст)
|
| 11 | ElytraMode: кольца из партиклов + детекция пролёта | `ElytraMode.java` | BoundingBox check в `tick()` |
|
||||||
|
| 12 | ElytraMode: cooldown на фейерверки | `ElytraMode.java` | Счётчик + сообщение |
|
||||||
### GravityShiftMode — визуальные эффекты
|
| 13 | RaceMode | новый файл | Кто первый наберёт N очков |
|
||||||
**Файл:** `mode/GravityShiftMode.java`
|
| 14 | CoopMode | новый файл | Общий счёт нескольких игроков |
|
||||||
- [ ] 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 <on|off>`
|
|
||||||
|
|
||||||
### Мультиплеер режимы
|
|
||||||
- [ ] RaceMode (гонка — кто первый наберёт N очков)
|
|
||||||
- [ ] CoopMode (кооператив — общий счёт)
|
|
||||||
|
|
||||||
### Локализация режимов
|
|
||||||
**Файлы:** `locales/en.yml`, `locales/ru.yml`
|
|
||||||
- [ ] gravity-shift: название эффекта, `effect-applied`
|
|
||||||
- [ ] elytra: `ring-passed`, `boost-ready`, `boost-cooldown`
|
|
||||||
- [ ] hardcore: `rewards-lost`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ Архитектурный рефакторинг — завершён
|
## ✅ Уже готово (архитектура)
|
||||||
|
|
||||||
| Что сделано | Результат |
|
- Все критические баги из предыдущих сессий исправлены (UUID, SQL race, PreparedStatement, Leaderboard, Scoreboard, World NPE)
|
||||||
|-------------|-----------|
|
- `ParkourGenerator` разбит: `GeneratorState` + `GeneratorLifecycle` + `BlockPlacer` + `EffectManager`
|
||||||
| `Command.java` → удалён (дубликат) | `LoParkourCommand.java` — единственный файл |
|
- `Option` делегирует в `SqlOptions` / `ParticleOptions` / `GenerationOptions`
|
||||||
| `LoParkourCommand.java` (528 строк) | `command/PlayerCommandHandler` + `AdminCommandHandler` + `SchematicCommandHandler` + тонкий роутер |
|
- `Option.SQL_URL/PORT/DB/USERNAME/PASSWORD/PREFIX` — forwarding-поля добавлены
|
||||||
| `Events.java` (268 строк) | `listener/PlayerConnectionListener` + `ParkourRestrictionListener` + `SchematicWandListener` |
|
- `GenerationOptions` — все новые пути защищены `isPath()` проверками
|
||||||
| `ParkourGenerator.java` (666 строк) | Делегирует в `EffectManager` + `GeneratorTick` |
|
- `LoParkourCommand` разбит на `PlayerCommandHandler` + `AdminCommandHandler` + `SchematicCommandHandler`
|
||||||
| `Option.java` (247 строк) | `SqlOptions` + `ParticleOptions` + `GenerationOptions` + тонкий `Option` |
|
- `Events` разбит на 3 listener'а
|
||||||
| GUI-паттерн (13 меню) | Исправлены на правильный `create()` API |
|
- `StorageSQL` разбит на `SQLConnectionManager` + `SQLQueryExecutor` + `SQLMigrationManager`
|
||||||
| Scoreboard | Полная реализация через Team prefix API |
|
|
||||||
| SQL инициализация | `pendingTableCreations` queue + `onConnectCallbacks` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎮 Чек-лист перед релизом
|
## 🎮 Чек-лист перед релизом
|
||||||
|
|
||||||
- [ ] `./gradlew build` без ошибок и предупреждений
|
- [ ] `./gradlew build` без ошибок
|
||||||
- [ ] Запуск сервера без ошибок в консоли
|
- [ ] Запуск сервера без ошибок в консоли
|
||||||
- [ ] Паркур создаётся и работает (прыжки, счёт, рекорд)
|
- [ ] Паркур: прыжки, счёт, рекорд работают
|
||||||
- [ ] Скорборд обновляется во время игры
|
- [ ] Scoreboard обновляется
|
||||||
- [ ] Все меню открываются корректно
|
- [ ] Все меню открываются
|
||||||
- [ ] SQL: таблицы создаются, данные сохраняются/читаются
|
- [ ] SQL: данные сохраняются и читаются
|
||||||
- [ ] Схематики: save → reload → paste работает
|
- [ ] Партиклы и звук в каждом режиме
|
||||||
- [ ] Лидерборд: запись и чтение рекордов
|
- [ ] Все 5 режимов запускаются без ошибок
|
||||||
- [ ] Частицы и звук работают в каждом режиме
|
|
||||||
- [ ] `/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
|
|
||||||
public enum VisualTheme {
|
|
||||||
NEON, // неоновые блоки + яркие партиклы
|
|
||||||
NATURE, // трава, листья, цветы
|
|
||||||
SPACE, // космос, звёзды
|
|
||||||
UNDERWATER,// вода, кораллы
|
|
||||||
HELL // нижний мир, лава
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Combo система
|
|
||||||
- Множитель очков за последовательные прыжки (x2, x3... x10)
|
|
||||||
- Визуальный индикатор combo
|
|
||||||
- Combo break при падении
|
|
||||||
- Бонусные очки за высокий combo
|
|
||||||
|
|
||||||
### Специальные прыжки
|
|
||||||
- **Neo-jump** - 4 блока по диагонали (5% шанс)
|
|
||||||
- **Head-hitter** - блок над головой (10% шанс)
|
|
||||||
- **Ladder jump** - прыжок на лестницу (15% шанс)
|
|
||||||
|
|
||||||
### Replay система
|
|
||||||
- Запись последнего прохождения
|
|
||||||
- Просмотр replay с камерой
|
|
||||||
- Экспорт/импорт replay файлов
|
|
||||||
- Замедление/ускорение воспроизведения
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📅 Временная шкала (6-7 недель)
|
|
||||||
|
|
||||||
| Фаза | Задачи | Время |
|
|
||||||
|------|--------|-------|
|
|
||||||
| 0 | Критические баги | 1-2 дня |
|
|
||||||
| 1 | Рефакторинг файлов | 1 неделя |
|
|
||||||
| 2 | Исправление фич | 1 неделя |
|
|
||||||
| 3 | Новые режимы | 2 недели |
|
|
||||||
| 4 | Визуал (темы, combo) | 1 неделя |
|
|
||||||
| 5 | Ghost система | 3 дня |
|
|
||||||
| 6 | Replay система | 1 неделя |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Критерии релиза 2.0.0
|
|
||||||
|
|
||||||
- ✅ Все файлы < 200 строк
|
|
||||||
- ✅ 5+ режимов игры
|
|
||||||
- ✅ Ghost система активна
|
|
||||||
- ✅ Визуальные темы
|
|
||||||
- ✅ Combo система
|
|
||||||
- ✅ Replay система
|
|
||||||
- ✅ Специальные прыжки
|
|
||||||
|
|
|
||||||
|
|
@ -57,30 +57,32 @@ public final class GenerationOptions {
|
||||||
|
|
||||||
static void init() {
|
static void init() {
|
||||||
// Jump validation
|
// Jump validation
|
||||||
JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled");
|
JUMP_VALIDATION_ENABLED = Config.CONFIG.isPath("jump-validation.enabled") && Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||||
MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance");
|
MAX_JUMP_DISTANCE = Config.CONFIG.isPath("jump-validation.max-distance") ? Config.CONFIG.getDouble("jump-validation.max-distance") : 5.0;
|
||||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal");
|
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.isPath("jump-validation.max-horizontal") ? Config.CONFIG.getDouble("jump-validation.max-horizontal") : 4.0;
|
||||||
MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up");
|
MAX_VERTICAL_UP = Config.CONFIG.isPath("jump-validation.max-vertical-up") ? Config.CONFIG.getDouble("jump-validation.max-vertical-up") : 1.0;
|
||||||
MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down");
|
MAX_VERTICAL_DOWN = Config.CONFIG.isPath("jump-validation.max-vertical-down") ? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : 3.0;
|
||||||
|
|
||||||
// Jump types
|
// Jump types
|
||||||
JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled");
|
JUMP_TYPES_ENABLED = Config.CONFIG.isPath("jump-types.enabled") && Config.CONFIG.getBoolean("jump-types.enabled");
|
||||||
JUMP_TYPE_ENABLED = new java.util.HashMap<>();
|
JUMP_TYPE_ENABLED = new java.util.HashMap<>();
|
||||||
JUMP_TYPE_CHANCE = new java.util.HashMap<>();
|
JUMP_TYPE_CHANCE = new java.util.HashMap<>();
|
||||||
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
|
if (Config.CONFIG.isPath("jump-types.types")) {
|
||||||
String path = "jump-types.types." + type;
|
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
|
||||||
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled"));
|
String path = "jump-types.types." + type;
|
||||||
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance"));
|
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.isPath(path + ".enabled") && Config.CONFIG.getBoolean(path + ".enabled"));
|
||||||
|
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.isPath(path + ".chance") ? Config.CONFIG.getDouble(path + ".chance") : 0.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory
|
// Memory
|
||||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance");
|
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance") ? Config.CONFIG.getInt("memory.block-cleanup-distance") : 100;
|
||||||
CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval");
|
CLEANUP_INTERVAL = Config.CONFIG.isPath("memory.cleanup-interval") ? Config.CONFIG.getInt("memory.cleanup-interval") : 100;
|
||||||
|
|
||||||
// Ghost
|
// Ghost
|
||||||
GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled");
|
GHOST_MODE_ENABLED = Config.CONFIG.isPath("ghost-mode.enabled") && Config.CONFIG.getBoolean("ghost-mode.enabled");
|
||||||
GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top");
|
GHOST_SHOW_TOP = Config.CONFIG.isPath("ghost-mode.show-top") ? Config.CONFIG.getInt("ghost-mode.show-top") : 3;
|
||||||
GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency");
|
GHOST_TRANSPARENCY = Config.CONFIG.isPath("ghost-mode.transparency") ? Config.CONFIG.getDouble("ghost-mode.transparency") : 0.5;
|
||||||
|
|
||||||
// Generation chances
|
// Generation chances
|
||||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,12 @@ public class Option {
|
||||||
|
|
||||||
/** @see SqlOptions#SQL */
|
/** @see SqlOptions#SQL */
|
||||||
public static boolean SQL;
|
public static boolean SQL;
|
||||||
|
public static String SQL_URL;
|
||||||
|
public static int SQL_PORT;
|
||||||
|
public static String SQL_DB;
|
||||||
|
public static String SQL_USERNAME;
|
||||||
|
public static String SQL_PASSWORD;
|
||||||
|
public static String SQL_PREFIX;
|
||||||
|
|
||||||
/** @see ParticleOptions#SHAPE */
|
/** @see ParticleOptions#SHAPE */
|
||||||
public static ParticleShape PARTICLE_SHAPE;
|
public static ParticleShape PARTICLE_SHAPE;
|
||||||
|
|
@ -87,7 +93,13 @@ public class Option {
|
||||||
|
|
||||||
/** Copy sub-class fields to this class so existing code still compiles. */
|
/** Copy sub-class fields to this class so existing code still compiles. */
|
||||||
private static void syncForwardedFields() {
|
private static void syncForwardedFields() {
|
||||||
SQL = SqlOptions.SQL;
|
SQL = SqlOptions.SQL;
|
||||||
|
SQL_URL = SqlOptions.URL;
|
||||||
|
SQL_PORT = SqlOptions.PORT;
|
||||||
|
SQL_DB = SqlOptions.DB;
|
||||||
|
SQL_USERNAME = SqlOptions.USERNAME;
|
||||||
|
SQL_PASSWORD = SqlOptions.PASSWORD;
|
||||||
|
SQL_PREFIX = SqlOptions.PREFIX;
|
||||||
|
|
||||||
PARTICLE_SHAPE = ParticleOptions.SHAPE;
|
PARTICLE_SHAPE = ParticleOptions.SHAPE;
|
||||||
SOUND_TYPE = ParticleOptions.SOUND_TYPE;
|
SOUND_TYPE = ParticleOptions.SOUND_TYPE;
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,13 @@ public class BlockPlacer {
|
||||||
|
|
||||||
/** Generates one block or schematic ahead. */
|
/** Generates one block or schematic ahead. */
|
||||||
public void generate() {
|
public void generate() {
|
||||||
if (g.waitForSchematicCompletion) return;
|
if (g.state.waitForSchematicCompletion) return;
|
||||||
|
|
||||||
Map<ParkourGenerator.BlockGenerationType, Double> chances = new HashMap<>(g.defaultChances);
|
Map<ParkourGenerator.BlockGenerationType, Double> chances = new HashMap<>(g.state.defaultChances);
|
||||||
if (g.schematicCooldown > 0
|
if (g.state.schematicCooldown > 0
|
||||||
|| g.generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS)
|
|| g.generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS)
|
||||||
|| g.profile.get("schematicDifficulty").asDouble() == 0.0
|
|| g.profile.get("schematicDifficulty").asDouble() == 0.0
|
||||||
|| !g.schematicBlocks.isEmpty()) {
|
|| !g.state.schematicBlocks.isEmpty()) {
|
||||||
chances.remove(ParkourGenerator.BlockGenerationType.SCHEMATIC);
|
chances.remove(ParkourGenerator.BlockGenerationType.SCHEMATIC);
|
||||||
}
|
}
|
||||||
if (!g.profile.get("useSpecialBlocks").asBoolean()) {
|
if (!g.profile.get("useSpecialBlocks").asBoolean()) {
|
||||||
|
|
@ -67,27 +67,27 @@ public class BlockPlacer {
|
||||||
|
|
||||||
/** Seeds the history and generates the initial lead. */
|
/** Seeds the history and generates the initial lead. */
|
||||||
public void generateFirst(Location spawn, Location blockSpawn) {
|
public void generateFirst(Location spawn, Location blockSpawn) {
|
||||||
g.playerSpawn = spawn;
|
g.state.playerSpawn = spawn;
|
||||||
g.lastStandingPlayerLocation = spawn;
|
g.state.lastStandingPlayerLocation = spawn;
|
||||||
g.blockSpawn = blockSpawn;
|
g.state.blockSpawn = blockSpawn;
|
||||||
g.history.add(blockSpawn.getBlock());
|
g.state.history.add(blockSpawn.getBlock());
|
||||||
generate(g.profile.get("blockLead").asInt());
|
generate(g.profile.get("blockLead").asInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears schematic blocks and resets cooldown. */
|
/** Clears schematic blocks and resets cooldown. */
|
||||||
public void deleteSchematic() {
|
public void deleteSchematic() {
|
||||||
if (!g.deleteSchematic) return;
|
if (!g.state.deleteSchematic) return;
|
||||||
g.schematicBlocks.forEach(b -> b.setType(Material.AIR));
|
g.state.schematicBlocks.forEach(b -> b.setType(Material.AIR));
|
||||||
g.schematicBlocks.clear();
|
g.state.schematicBlocks.clear();
|
||||||
g.deleteSchematic = false;
|
g.state.deleteSchematic = false;
|
||||||
g.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
g.state.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Block selection ────────────────────────────────────────────────────────
|
// ── Block selection ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
protected List<Block> selectBlocks() {
|
protected List<Block> selectBlocks() {
|
||||||
int height = Probs.random(g.heightChances);
|
int height = Probs.random(g.state.heightChances);
|
||||||
int distance = Probs.random(g.distanceChances);
|
int distance = Probs.random(g.state.distanceChances);
|
||||||
return List.of(selectNext(g.getLatest(), distance, height));
|
return List.of(selectNext(g.getLatest(), distance, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ public class BlockPlacer {
|
||||||
JumpDirector director = new JumpDirector(
|
JumpDirector director = new JumpDirector(
|
||||||
BoundingBox.of(g.zone[0], g.zone[1]), g.getLatest().getLocation().toVector());
|
BoundingBox.of(g.zone[0], g.zone[1]), g.getLatest().getLocation().toVector());
|
||||||
|
|
||||||
g.heading = director.getRecommendedHeading(g.heading);
|
g.state.heading = director.getRecommendedHeading(g.state.heading);
|
||||||
height = director.getRecommendedHeight(height);
|
height = director.getRecommendedHeight(height);
|
||||||
|
|
||||||
switch (g.getLatest().getType()) {
|
switch (g.getLatest().getType()) {
|
||||||
|
|
@ -107,11 +107,11 @@ public class BlockPlacer {
|
||||||
double sd = g.generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
|
double sd = g.generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
|
||||||
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd);
|
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd);
|
||||||
|
|
||||||
Vector offset = g.heading.clone().multiply(distance + 1).setY(height);
|
Vector offset = g.state.heading.clone().multiply(distance + 1).setY(height);
|
||||||
if (offset.getX() == 0) offset.setX(randomOffset);
|
if (offset.getX() == 0) offset.setX(randomOffset);
|
||||||
else offset.setZ(randomOffset);
|
else offset.setZ(randomOffset);
|
||||||
|
|
||||||
offset.rotateAroundY(angleInY(g.heading, Option.HEADING.getDirection()));
|
offset.rotateAroundY(angleInY(g.state.heading, Option.HEADING.getDirection()));
|
||||||
return current.getLocation().add(offset).getBlock();
|
return current.getLocation().add(offset).getBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ public class BlockPlacer {
|
||||||
for (Block block : blocks) {
|
for (Block block : blocks) {
|
||||||
BlockData data = (jump == ParkourGenerator.BlockGenerationType.SPECIAL
|
BlockData data = (jump == ParkourGenerator.BlockGenerationType.SPECIAL
|
||||||
&& !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL))
|
&& !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL))
|
||||||
? Probs.random(g.specialChances)
|
? Probs.random(g.state.specialChances)
|
||||||
: selectBlockData();
|
: selectBlockData();
|
||||||
|
|
||||||
if (data instanceof Fence) block = block.getLocation().subtract(0, 1, 0).getBlock();
|
if (data instanceof Fence) block = block.getLocation().subtract(0, 1, 0).getBlock();
|
||||||
|
|
@ -148,8 +148,8 @@ public class BlockPlacer {
|
||||||
new ParkourBlockGenerateEvent(placed, g, g.player).call();
|
new ParkourBlockGenerateEvent(placed, g, g.player).call();
|
||||||
g.effects.particles(placed);
|
g.effects.particles(placed);
|
||||||
g.effects.sound(placed);
|
g.effects.sound(placed);
|
||||||
g.history.addAll(placed);
|
g.state.history.addAll(placed);
|
||||||
g.schematicCooldown--;
|
g.state.schematicCooldown--;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryGenerateSchematic() {
|
private boolean tryGenerateSchematic() {
|
||||||
|
|
@ -166,14 +166,14 @@ public class BlockPlacer {
|
||||||
var pool = candidates.isEmpty() ? new ArrayList<>(all.values()) : candidates;
|
var pool = candidates.isEmpty() ? new ArrayList<>(all.values()) : candidates;
|
||||||
var schematic = pool.get(new java.util.Random().nextInt(pool.size()));
|
var schematic = pool.get(new java.util.Random().nextInt(pool.size()));
|
||||||
|
|
||||||
Location origin = g.getLatest().getLocation().add(g.heading.clone().multiply(2));
|
Location origin = g.getLatest().getLocation().add(g.state.heading.clone().multiply(2));
|
||||||
List<Block> placed = rotatedPaste(schematic, origin);
|
List<Block> placed = rotatedPaste(schematic, origin);
|
||||||
if (placed.isEmpty()) return false;
|
if (placed.isEmpty()) return false;
|
||||||
|
|
||||||
g.schematicBlocks.addAll(placed);
|
g.state.schematicBlocks.addAll(placed);
|
||||||
g.history.addAll(placed);
|
g.state.history.addAll(placed);
|
||||||
g.waitForSchematicCompletion = true;
|
g.state.waitForSchematicCompletion = true;
|
||||||
g.deleteSchematic = false;
|
g.state.deleteSchematic = false;
|
||||||
|
|
||||||
new ParkourSchematicGenerateEvent(schematic, g, g.player).call();
|
new ParkourSchematicGenerateEvent(schematic, g, g.player).call();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,120 +1,7 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
|
||||||
import dev.loki.loparkour.config.Option;
|
|
||||||
import dev.loki.loparkour.style.Style;
|
|
||||||
import dev.loki.loparkour.util.Probs;
|
|
||||||
import org.bukkit.Location;
|
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.block.Block;
|
|
||||||
import org.bukkit.block.data.BlockData;
|
|
||||||
import org.bukkit.util.BoundingBox;
|
|
||||||
import org.bukkit.util.Vector;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects next parkour blocks (SRP - Single Responsibility)
|
* @deprecated Logic merged into {@link BlockPlacer}. Delete this file.
|
||||||
*/
|
*/
|
||||||
public class BlockSelector {
|
@Deprecated
|
||||||
|
class BlockSelector {}
|
||||||
private final Profile profile;
|
|
||||||
private final Location[] zone;
|
|
||||||
private final Vector initialHeading;
|
|
||||||
private final Map<Integer, Double> heightChances = new HashMap<>();
|
|
||||||
private final Map<Integer, Double> distanceChances = new HashMap<>();
|
|
||||||
|
|
||||||
private Vector heading;
|
|
||||||
|
|
||||||
public BlockSelector(Profile profile, Location[] zone, Vector initialHeading) {
|
|
||||||
this.profile = profile;
|
|
||||||
this.zone = zone;
|
|
||||||
this.heading = initialHeading.clone();
|
|
||||||
this.initialHeading = initialHeading.clone();
|
|
||||||
|
|
||||||
calculateChances();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void calculateChances() {
|
|
||||||
heightChances.put(1, Option.NORMAL_HEIGHT_1);
|
|
||||||
heightChances.put(0, Option.NORMAL_HEIGHT_0);
|
|
||||||
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
|
||||||
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
|
||||||
|
|
||||||
distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
|
||||||
distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
|
||||||
distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
|
||||||
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Block> selectBlocks(Block latest) {
|
|
||||||
int height = Probs.random(heightChances);
|
|
||||||
int distance = Probs.random(distanceChances);
|
|
||||||
return List.of(selectNext(latest, distance, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Block selectNext(Block current, int distance, int height) {
|
|
||||||
JumpDirector director = new JumpDirector(
|
|
||||||
BoundingBox.of(zone[0], zone[1]),
|
|
||||||
current.getLocation().toVector()
|
|
||||||
);
|
|
||||||
|
|
||||||
heading = director.getRecommendedHeading(heading);
|
|
||||||
height = director.getRecommendedHeight(height);
|
|
||||||
|
|
||||||
// Ensure special blocks are possible
|
|
||||||
switch (current.getType()) {
|
|
||||||
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
|
|
||||||
case GLASS_PANE -> distance = Math.min(distance, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height > 0) {
|
|
||||||
distance = Math.max(distance - height, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, 1);
|
|
||||||
|
|
||||||
Vector offset = heading.clone()
|
|
||||||
.multiply(distance + 1)
|
|
||||||
.setY(height);
|
|
||||||
|
|
||||||
if (offset.getX() == 0) {
|
|
||||||
offset.setX(randomOffset);
|
|
||||||
} else {
|
|
||||||
offset.setZ(randomOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
offset.rotateAroundY(angleInY(heading, initialHeading));
|
|
||||||
|
|
||||||
return current.getLocation().add(offset).getBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlockData selectBlockData() {
|
|
||||||
Style style = Registry.getStyle(profile.get("style").value());
|
|
||||||
|
|
||||||
if (style == null) {
|
|
||||||
profile.set("style", Registry.getStyles().stream()
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow()
|
|
||||||
.getName());
|
|
||||||
return selectBlockData();
|
|
||||||
}
|
|
||||||
|
|
||||||
return style.getNext().createBlockData();
|
|
||||||
}
|
|
||||||
|
|
||||||
private double angleInY(Vector a, Vector b) {
|
|
||||||
double det = a.getX() * b.getZ() - a.getZ() * b.getX();
|
|
||||||
return Math.atan2(det, a.dot(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector getHeading() {
|
|
||||||
return heading;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetHeading() {
|
|
||||||
heading = initialHeading.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ public final class Island {
|
||||||
|
|
||||||
// First parkour block should be generated FROM the parkour start block
|
// First parkour block should be generated FROM the parkour start block
|
||||||
// in the direction of heading, not AT the parkour start block
|
// in the direction of heading, not AT the parkour start block
|
||||||
Location parkourStart = parkour.getLocation().add(session.generator.heading);
|
Location parkourStart = parkour.getLocation().add(session.generator.state.heading);
|
||||||
|
|
||||||
session.generator.generateFirst(ps, parkourStart);
|
session.generator.generateFirst(ps, parkourStart);
|
||||||
session.generator.startTick();
|
session.generator.startTick();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator;
|
||||||
|
|
||||||
import dev.lolib.scheduler.Scheduler;
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.api.Registry;
|
|
||||||
import dev.loki.loparkour.api.event.ParkourFallEvent;
|
|
||||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.Option;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
|
|
@ -14,15 +10,12 @@ import dev.loki.loparkour.mode.Mode;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.Modes;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.ParkourSpectator;
|
import dev.loki.loparkour.player.ParkourSpectator;
|
||||||
import dev.loki.loparkour.reward.Rewards;
|
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.Session;
|
||||||
import dev.loki.loparkour.world.Divider;
|
import dev.loki.loparkour.world.Divider;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
|
||||||
import org.bukkit.util.Vector;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
|
@ -31,61 +24,23 @@ import java.time.ZoneOffset;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
|
||||||
* Coordinates parkour generation, ticking, scoring, and reset.
|
|
||||||
*
|
|
||||||
* Heavy lifting is delegated to:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link BlockPlacer} – block selection & placement</li>
|
|
||||||
* <li>{@link EffectManager} – particles & sounds</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public class ParkourGenerator {
|
public class ParkourGenerator {
|
||||||
|
|
||||||
public static final int BLOCK_TRAIL = 2;
|
public static final int BLOCK_TRAIL = 2;
|
||||||
|
|
||||||
// ── State ──────────────────────────────────────────────────────────────────
|
public final GeneratorState state = new GeneratorState();
|
||||||
public int score = 0;
|
public final GeneratorLifecycle lifecycle;
|
||||||
public int totalScore = 0;
|
|
||||||
public int schematicCooldown;
|
|
||||||
public boolean stopped = false;
|
|
||||||
|
|
||||||
public Location[] zone;
|
|
||||||
public ParkourPlayer player;
|
|
||||||
public dev.lolib.scheduler.ScheduledTask task;
|
|
||||||
private dev.lolib.scheduler.ScheduledTask cleanupTask;
|
|
||||||
|
|
||||||
public Location blockSpawn;
|
|
||||||
public Location playerSpawn;
|
|
||||||
public Instant start;
|
|
||||||
public Vector heading = Option.HEADING.getDirection();
|
|
||||||
|
|
||||||
public final List<GeneratorOption> generatorOptions;
|
|
||||||
public final Session session;
|
|
||||||
public final Profile profile = new Profile();
|
|
||||||
public final Island island;
|
|
||||||
|
|
||||||
// Chance maps – populated by calculateChances()
|
|
||||||
public final Map<Integer, Double> distanceChances = new HashMap<>();
|
|
||||||
public final Map<Integer, Double> heightChances = new HashMap<>();
|
|
||||||
public final Map<BlockData, Double> specialChances = new HashMap<>();
|
|
||||||
public final Map<BlockGenerationType, Double> defaultChances = new HashMap<>();
|
|
||||||
|
|
||||||
// Schematic state
|
|
||||||
public boolean deleteSchematic = false;
|
|
||||||
public boolean waitForSchematicCompletion = false;
|
|
||||||
public List<Block> schematicBlocks = new ArrayList<>();
|
|
||||||
|
|
||||||
// History
|
|
||||||
public List<Block> history = new LinkedList<>();
|
|
||||||
public int lastPositionIndexPlayer = -1;
|
|
||||||
public Location lastStandingPlayerLocation;
|
|
||||||
|
|
||||||
// Delegates
|
|
||||||
public final EffectManager effects;
|
public final EffectManager effects;
|
||||||
public final BlockPlacer placer;
|
public final BlockPlacer placer;
|
||||||
|
|
||||||
// ── Constructors ───────────────────────────────────────────────────────────
|
public final Session session;
|
||||||
|
public final ParkourPlayer player;
|
||||||
|
public final Profile profile = new Profile();
|
||||||
|
public final Island island;
|
||||||
|
public final List<GeneratorOption> generatorOptions;
|
||||||
|
|
||||||
|
public Location[] zone;
|
||||||
|
public dev.lolib.scheduler.ScheduledTask task;
|
||||||
|
|
||||||
public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic,
|
public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic,
|
||||||
GeneratorOption... generatorOptions) {
|
GeneratorOption... generatorOptions) {
|
||||||
|
|
@ -94,9 +49,13 @@ public class ParkourGenerator {
|
||||||
this.player = session.getPlayers().get(0);
|
this.player = session.getPlayers().get(0);
|
||||||
this.island = new Island(session, schematic);
|
this.island = new Island(session, schematic);
|
||||||
this.zone = Divider.toSelection(session);
|
this.zone = Divider.toSelection(session);
|
||||||
this.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
this.state.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||||
|
this.state.heading = Option.HEADING.getDirection();
|
||||||
|
|
||||||
this.effects = new EffectManager(this);
|
this.effects = new EffectManager(this);
|
||||||
this.placer = new BlockPlacer(this);
|
this.placer = new BlockPlacer(this);
|
||||||
|
this.lifecycle = new GeneratorLifecycle(this);
|
||||||
|
|
||||||
calculateChances();
|
calculateChances();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,211 +69,100 @@ public class ParkourGenerator {
|
||||||
var manager = LoParkour.getSchematicManager();
|
var manager = LoParkour.getSchematicManager();
|
||||||
if (manager == null) return null;
|
if (manager == null) return null;
|
||||||
LPSchematic s = manager.getSchematic(name);
|
LPSchematic s = manager.getSchematic(name);
|
||||||
if (s == null) LoParkour.getPlugin().getLogger()
|
if (s == null) {
|
||||||
.warning("Island schematic '%s' not found!".formatted(name));
|
LoParkour.getPlugin().getLogger().warning("Island schematic '%s' not found!".formatted(name));
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void overrideProfile() {}
|
public void overrideProfile() {}
|
||||||
|
|
||||||
// ── Chances ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
protected void calculateChances() {
|
protected void calculateChances() {
|
||||||
defaultChances.clear();
|
state.defaultChances.clear();
|
||||||
defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
state.defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
||||||
defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
state.defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
||||||
defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
state.defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
||||||
|
|
||||||
heightChances.clear();
|
state.heightChances.clear();
|
||||||
heightChances.put( 1, Option.NORMAL_HEIGHT_1);
|
state.heightChances.put(1, Option.NORMAL_HEIGHT_1);
|
||||||
heightChances.put( 0, Option.NORMAL_HEIGHT_0);
|
state.heightChances.put(0, Option.NORMAL_HEIGHT_0);
|
||||||
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
state.heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
||||||
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
state.heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
||||||
|
|
||||||
distanceChances.clear();
|
state.distanceChances.clear();
|
||||||
distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
state.distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
||||||
distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
state.distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
||||||
distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
state.distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
||||||
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
state.distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
||||||
|
|
||||||
specialChances.clear();
|
state.specialChances.clear();
|
||||||
specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
|
state.specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
|
||||||
specialChances.put(Material.SMOOTH_QUARTZ_SLAB.createBlockData("[type=bottom]"), Option.SPECIAL_SLAB);
|
state.specialChances.put(Material.SMOOTH_QUARTZ_SLAB.createBlockData("[type=bottom]"), Option.SPECIAL_SLAB);
|
||||||
specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
|
state.specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
|
||||||
specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
|
state.specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Generation (delegates) ─────────────────────────────────────────────────
|
public void generate() { placer.generate(); }
|
||||||
|
public void generate(int amount) { placer.generate(amount); }
|
||||||
|
public void generateFirst(Location spawn, Location block) { placer.generateFirst(spawn, block); }
|
||||||
|
|
||||||
public void generate() { placer.generate(); }
|
public void startTick() { lifecycle.startTick(); }
|
||||||
public void generate(int amount) { placer.generate(amount); }
|
public void tick() { lifecycle.tick(); }
|
||||||
|
protected void fall() { lifecycle.fall(); }
|
||||||
public void generateFirst(Location spawn, Location block) {
|
protected void score() { lifecycle.score(); }
|
||||||
placer.generateFirst(spawn, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tick ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public void startTick() {
|
|
||||||
task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
|
|
||||||
cleanupTask = Scheduler.get(LoParkour.getPlugin()).runTimer(this::cleanupDistantBlocks, 0, Option.CLEANUP_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void tick() {
|
|
||||||
if (stopped) { task.cancel(); if (cleanupTask != null) cleanupTask.cancel(); return; }
|
|
||||||
|
|
||||||
getPlayers().forEach(p -> {
|
|
||||||
updateVisualTime(p, p.selectedTime);
|
|
||||||
p.updateScoreboard(this);
|
|
||||||
p.player.setSaturation(20);
|
|
||||||
});
|
|
||||||
getSpectators().forEach(ParkourSpectator::update);
|
|
||||||
|
|
||||||
if (player.getLocation().getWorld() != lastStandingPlayerLocation.getWorld()) return;
|
|
||||||
if (player.getLocation().subtract(lastStandingPlayerLocation).getY() < -10) { fall(); return; }
|
|
||||||
|
|
||||||
Block below = blockBelow();
|
|
||||||
if (below == null) return;
|
|
||||||
|
|
||||||
// Schematic end block
|
|
||||||
if (schematicBlocks.contains(below) && below.getType() == Material.RED_WOOL && !deleteSchematic) {
|
|
||||||
for (int i = 0; i < profile.get("schematicDifficulty").asDouble() * 15; i++) score();
|
|
||||||
waitForSchematicCompletion = false;
|
|
||||||
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
|
||||||
generate(profile.get("blockLead").asInt());
|
|
||||||
deleteSchematic = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!history.contains(below)) return;
|
|
||||||
|
|
||||||
int idx = history.indexOf(below);
|
|
||||||
int delta = idx - lastPositionIndexPlayer;
|
|
||||||
if (delta <= 0) return;
|
|
||||||
|
|
||||||
lastStandingPlayerLocation = player.getLocation();
|
|
||||||
|
|
||||||
int lead = profile.get("blockLead").asInt();
|
|
||||||
if (history.size() - idx <= lead) generate(lead - (history.size() - idx));
|
|
||||||
lastPositionIndexPlayer = idx;
|
|
||||||
|
|
||||||
// Remove trail blocks behind player
|
|
||||||
for (int i = idx - BLOCK_TRAIL - 1; i >= idx - 4 * BLOCK_TRAIL; i--) {
|
|
||||||
if (i > 0) history.get(i).setType(Material.AIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupDistantBlocks();
|
|
||||||
placer.deleteSchematic();
|
|
||||||
|
|
||||||
int pts = Config.CONFIG.getBoolean("scoring.all-points") ? delta : 1;
|
|
||||||
for (int i = 0; i < pts; i++) score();
|
|
||||||
|
|
||||||
if (start == null) start = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable Block blockBelow() {
|
|
||||||
Location loc = player.getLocation().subtract(0, 1, 0);
|
|
||||||
Block b = loc.getBlock();
|
|
||||||
if (b.getType() == Material.AIR) {
|
|
||||||
if (loc.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) return null;
|
|
||||||
b = loc.getBlock();
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateVisualTime(ParkourPlayer p, int selectedTime) {
|
|
||||||
int t = 18000 + selectedTime;
|
|
||||||
if (t >= 24000) t -= 24000;
|
|
||||||
p.player.setPlayerTime(t, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Score ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
protected void score() {
|
|
||||||
score++;
|
|
||||||
totalScore++;
|
|
||||||
checkRewards();
|
|
||||||
new ParkourScoreEvent(player).call();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkRewards() {
|
|
||||||
if (!Rewards.REWARDS_ENABLED || score == 0) return;
|
|
||||||
|
|
||||||
if (Rewards.SCORE_REWARDS.containsKey(score))
|
|
||||||
Rewards.SCORE_REWARDS.get(score).forEach(r -> r.execute(player, getMode()));
|
|
||||||
|
|
||||||
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
|
|
||||||
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
|
|
||||||
if (intervalScore % interval == 0)
|
|
||||||
Rewards.INTERVAL_REWARDS.get(interval).forEach(r -> r.execute(player, getMode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
String key = Integer.toString(score);
|
|
||||||
if (Rewards.ONE_TIME_REWARDS.containsKey(score) && !player.collectedRewards.contains(key)) {
|
|
||||||
Rewards.ONE_TIME_REWARDS.get(score).forEach(r -> r.execute(player, getMode()));
|
|
||||||
player.collectedRewards.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Fall / Reset ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
protected void fall() {
|
|
||||||
new ParkourFallEvent(player).call();
|
|
||||||
reset(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset(boolean regenerate) {
|
public void reset(boolean regenerate) {
|
||||||
stopped = !regenerate;
|
state.stopped = !regenerate;
|
||||||
|
if (!regenerate && task == null) {
|
||||||
if (!regenerate && task == null)
|
|
||||||
LoParkour.getPlugin().getLogger().warning("## Incomplete joining setup — report this!");
|
LoParkour.getPlugin().getLogger().warning("## Incomplete joining setup — report this!");
|
||||||
|
|
||||||
lastPositionIndexPlayer = 0;
|
|
||||||
if (!history.isEmpty()) {
|
|
||||||
history.remove(0);
|
|
||||||
history.forEach(b -> b.setType(Material.AIR, false));
|
|
||||||
history.clear();
|
|
||||||
}
|
}
|
||||||
|
state.lastPositionIndexPlayer = 0;
|
||||||
waitForSchematicCompletion = false;
|
if (!state.history.isEmpty()) {
|
||||||
deleteSchematic = true;
|
state.history.remove(0);
|
||||||
|
state.history.forEach(b -> b.setType(Material.AIR, false));
|
||||||
|
state.history.clear();
|
||||||
|
}
|
||||||
|
state.resetSchematicState();
|
||||||
placer.deleteSchematic();
|
placer.deleteSchematic();
|
||||||
|
|
||||||
Leaderboard lb = getMode().getLeaderboard();
|
Leaderboard lb = getMode().getLeaderboard();
|
||||||
int record = lb != null ? lb.get(player.getUUID()).score() : 0;
|
int record = lb != null ? lb.get(player.getUUID()).score() : 0;
|
||||||
|
if (profile.get("showFallMessage").asBoolean()) {
|
||||||
if (profile.get("showFallMessage").asBoolean()) sendFallMessage(record);
|
sendFallMessage(record);
|
||||||
if (lb != null && score > record)
|
}
|
||||||
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), score);
|
if (lb != null && state.score > record) {
|
||||||
|
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), state.score);
|
||||||
score = 0;
|
}
|
||||||
start = null;
|
state.resetScore();
|
||||||
heading = Option.HEADING.getDirection();
|
state.heading = Option.HEADING.getDirection();
|
||||||
|
|
||||||
if (regenerate) {
|
if (regenerate) {
|
||||||
player.teleport(playerSpawn);
|
player.teleport(state.playerSpawn);
|
||||||
generateFirst(playerSpawn, blockSpawn);
|
generateFirst(state.playerSpawn, state.blockSpawn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
island.destroy();
|
island.destroy();
|
||||||
if (getPlayers().isEmpty())
|
if (getPlayers().isEmpty()) {
|
||||||
getSpectators().forEach(s -> Modes.DEFAULT.create(s.player));
|
getSpectators().forEach(s -> Modes.DEFAULT.create(s.player));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendFallMessage(int record) {
|
private void sendFallMessage(int record) {
|
||||||
String key;
|
String key;
|
||||||
int number = 0;
|
int number = 0;
|
||||||
if (score == record) key = "settings.parkour_settings.items.fall_message.formats.tied";
|
if (state.score == record) {
|
||||||
else if (score > record) { key = "settings.parkour_settings.items.fall_message.formats.beat"; number = score - record; }
|
key = "settings.parkour_settings.items.fall_message.formats.tied";
|
||||||
else { key = "settings.parkour_settings.items.fall_message.formats.miss"; number = record - score; }
|
} else if (state.score > record) {
|
||||||
|
key = "settings.parkour_settings.items.fall_message.formats.beat";
|
||||||
|
number = state.score - record;
|
||||||
|
} else {
|
||||||
|
key = "settings.parkour_settings.items.fall_message.formats.miss";
|
||||||
|
number = record - state.score;
|
||||||
|
}
|
||||||
for (ParkourPlayer p : getPlayers()) {
|
for (ParkourPlayer p : getPlayers()) {
|
||||||
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
||||||
p.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(score));
|
p.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(state.score));
|
||||||
p.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
|
p.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
|
||||||
p.sendTranslated("settings.parkour_settings.items.fall_message.high_score", Integer.toString(record));
|
p.sendTranslated("settings.parkour_settings.items.fall_message.high_score", Integer.toString(record));
|
||||||
p.sendTranslated(key, Integer.toString(number));
|
p.sendTranslated(key, Integer.toString(number));
|
||||||
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,29 +173,8 @@ public class ParkourGenerator {
|
||||||
getPlayers().forEach(p -> lb.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
|
getPlayers().forEach(p -> lb.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cleanup ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
protected void cleanupDistantBlocks() {
|
|
||||||
if (history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) return;
|
|
||||||
Location loc = player.getLocation();
|
|
||||||
int removed = 0;
|
|
||||||
Iterator<Block> it = history.iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
Block b = it.next();
|
|
||||||
if (b.getLocation().distance(loc) > Option.BLOCK_CLEANUP_DISTANCE) {
|
|
||||||
b.setType(Material.AIR, false);
|
|
||||||
it.remove();
|
|
||||||
removed++;
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
if (removed > 0) lastPositionIndexPlayer = Math.max(0, lastPositionIndexPlayer - removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Misc ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public void menu(ParkourPlayer player) { Menus.PARKOUR_SETTINGS.open(player); }
|
public void menu(ParkourPlayer player) { Menus.PARKOUR_SETTINGS.open(player); }
|
||||||
|
public Block getLatest() { return state.history.get(state.history.size() - 1); }
|
||||||
public Block getLatest() { return history.get(history.size() - 1); }
|
|
||||||
|
|
||||||
public double getDifficultyScore() {
|
public double getDifficultyScore() {
|
||||||
double s = 0;
|
double s = 0;
|
||||||
|
|
@ -357,11 +184,16 @@ public class ParkourGenerator {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFormattedTime() { return getTime(Config.CONFIG.getString("options.time.score-format")); }
|
public String getFormattedTime() {
|
||||||
public String getDetailedTime() { return getTime("mm:ss:SSS"); }
|
return getTime(Config.CONFIG.getString("options.time.score-format"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDetailedTime() {
|
||||||
|
return getTime("mm:ss:SSS");
|
||||||
|
}
|
||||||
|
|
||||||
private String getTime(String format) {
|
private String getTime(String format) {
|
||||||
var ms = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
|
var ms = Instant.now().minusMillis(state.start != null ? state.start.toEpochMilli() : Instant.now().toEpochMilli());
|
||||||
try {
|
try {
|
||||||
return DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC).format(ms);
|
return DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC).format(ms);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
|
|
@ -371,8 +203,7 @@ public class ParkourGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mode getMode() { return Modes.DEFAULT; }
|
public Mode getMode() { return Modes.DEFAULT; }
|
||||||
|
@NotNull public List<ParkourPlayer> getPlayers() { return session.getPlayers(); }
|
||||||
@NotNull public List<ParkourPlayer> getPlayers() { return session.getPlayers(); }
|
|
||||||
@NotNull public List<ParkourSpectator> getSpectators() { return session.getSpectators(); }
|
@NotNull public List<ParkourSpectator> getSpectators() { return session.getSpectators(); }
|
||||||
|
|
||||||
public enum BlockGenerationType { DEFAULT, SCHEMATIC, SPECIAL }
|
public enum BlockGenerationType { DEFAULT, SCHEMATIC, SPECIAL }
|
||||||
|
|
|
||||||
|
|
@ -1,126 +1,8 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
|
||||||
import dev.loki.loparkour.config.Config;
|
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
|
||||||
import dev.loki.loparkour.mode.Mode;
|
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
|
||||||
import dev.loki.loparkour.reward.Rewards;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages scoring and rewards (SRP - Single Responsibility)
|
* @deprecated Score logic lives in {@link GeneratorLifecycle}.
|
||||||
|
* This class was created but never wired in. Delete this file.
|
||||||
*/
|
*/
|
||||||
public class ScoreManager {
|
@Deprecated
|
||||||
|
class ScoreManager {}
|
||||||
private final ParkourPlayer player;
|
|
||||||
private final Mode mode;
|
|
||||||
private final Profile profile;
|
|
||||||
|
|
||||||
private int score = 0;
|
|
||||||
private int totalScore = 0;
|
|
||||||
private Instant start;
|
|
||||||
|
|
||||||
public ScoreManager(ParkourPlayer player, Mode mode, Profile profile) {
|
|
||||||
this.player = player;
|
|
||||||
this.mode = mode;
|
|
||||||
this.profile = profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addScore(int points) {
|
|
||||||
for (int i = 0; i < points; i++) {
|
|
||||||
score++;
|
|
||||||
totalScore++;
|
|
||||||
checkRewards();
|
|
||||||
new ParkourScoreEvent(player).call();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start == null) {
|
|
||||||
start = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkRewards() {
|
|
||||||
if (!Rewards.REWARDS_ENABLED || score == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Rewards.SCORE_REWARDS.containsKey(score)) {
|
|
||||||
Rewards.SCORE_REWARDS.get(score).forEach(s -> s.execute(player, mode));
|
|
||||||
}
|
|
||||||
|
|
||||||
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
|
|
||||||
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
|
|
||||||
if (intervalScore % interval != 0) continue;
|
|
||||||
Rewards.INTERVAL_REWARDS.get(interval).forEach(s -> s.execute(player, mode));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Rewards.ONE_TIME_REWARDS.containsKey(score) &&
|
|
||||||
!player.collectedRewards.contains(Integer.toString(score))) {
|
|
||||||
Rewards.ONE_TIME_REWARDS.get(score).forEach(s -> s.execute(player, mode));
|
|
||||||
player.collectedRewards.add(Integer.toString(score));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset() {
|
|
||||||
score = 0;
|
|
||||||
start = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerScore(List<ParkourPlayer> players) {
|
|
||||||
Leaderboard leaderboard = mode.getLeaderboard();
|
|
||||||
if (leaderboard == null) return;
|
|
||||||
|
|
||||||
String time = getDetailedTime();
|
|
||||||
String difficulty = Double.toString(getDifficultyScore()).substring(0, Math.min(3, Double.toString(getDifficultyScore()).length()));
|
|
||||||
|
|
||||||
players.forEach(p -> leaderboard.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFormattedTime() {
|
|
||||||
return getTime(Config.CONFIG.getString("options.time.score-format"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDetailedTime() {
|
|
||||||
return getTime("mm:ss:SSS");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTime(String format) {
|
|
||||||
var timeMs = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
|
|
||||||
|
|
||||||
try {
|
|
||||||
return DateTimeFormatter.ofPattern(format)
|
|
||||||
.withZone(ZoneOffset.UTC)
|
|
||||||
.format(timeMs);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getDifficultyScore() {
|
|
||||||
double score = 0;
|
|
||||||
if (profile.get("useSpecialBlocks").asBoolean()) score += 0.5;
|
|
||||||
|
|
||||||
double schematicDiff = profile.get("schematicDifficulty").asDouble();
|
|
||||||
if (schematicDiff > 0) {
|
|
||||||
if (schematicDiff <= 0.25) score += 0.2;
|
|
||||||
else if (schematicDiff <= 0.5) score += 0.3;
|
|
||||||
else if (schematicDiff <= 0.75) score += 0.4;
|
|
||||||
else score += 0.5;
|
|
||||||
}
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getScore() {
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalScore() {
|
|
||||||
return totalScore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
29
src/main/java/dev/loki/loparkour/player/BungeeUtil.java
Normal file
29
src/main/java/dev/loki/loparkour/player/BungeeUtil.java
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package dev.loki.loparkour.player;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteArrayDataOutput;
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for BungeeCord operations.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class BungeeUtil {
|
||||||
|
|
||||||
|
public static void sendPlayerToServer(Player player, String server) {
|
||||||
|
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||||
|
out.writeUTF("Connect");
|
||||||
|
out.writeUTF(server);
|
||||||
|
|
||||||
|
try {
|
||||||
|
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
|
||||||
|
} catch (ChannelNotRegisteredException ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error while trying to send %s to server %s. - %s".formatted(player.getName(), server, ex.getMessage()));
|
||||||
|
player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,33 +36,7 @@ import java.util.function.BiConsumer;
|
||||||
*/
|
*/
|
||||||
public class ParkourPlayer extends ParkourUser {
|
public class ParkourPlayer extends ParkourUser {
|
||||||
|
|
||||||
public static final Map<String, OptionContainer> PLAYER_COLUMNS = new HashMap<>();
|
public static final Map<String, OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
|
||||||
|
|
||||||
static {
|
|
||||||
PLAYER_COLUMNS.put("uuid", new OptionContainer(null, null));
|
|
||||||
PLAYER_COLUMNS.put("style", new OptionContainer(ParkourOption.STYLES, (player, v) -> player.style = v));
|
|
||||||
PLAYER_COLUMNS.put("blockLead", new OptionContainer(ParkourOption.LEADS, (player, v) -> player.blockLead = Integer.parseInt(v)));
|
|
||||||
PLAYER_COLUMNS.put("useParticles", new OptionContainer(ParkourOption.PARTICLES, (player, v) -> player.particles = parseBoolean(v)));
|
|
||||||
PLAYER_COLUMNS.put("useSpecial", new OptionContainer(ParkourOption.SPECIAL_BLOCKS, (player, v) -> player.useSpecialBlocks = parseBoolean(v)));
|
|
||||||
PLAYER_COLUMNS.put("showFallMsg", new OptionContainer(ParkourOption.FALL_MESSAGE, (player, v) -> player.showFallMessage = parseBoolean(v)));
|
|
||||||
PLAYER_COLUMNS.put("showScoreboard", new OptionContainer(ParkourOption.SCOREBOARD, (player, v) -> player.showScoreboard = parseBoolean(v)));
|
|
||||||
PLAYER_COLUMNS.put("selectedTime", new OptionContainer(ParkourOption.TIME, (player, v) -> player.selectedTime = Integer.parseInt(v)));
|
|
||||||
PLAYER_COLUMNS.put("collectedRewards", new OptionContainer(null, (player, v) -> {
|
|
||||||
player.collectedRewards = new ArrayList<>();
|
|
||||||
|
|
||||||
if (!v.isEmpty()) {
|
|
||||||
player.collectedRewards.addAll(Arrays.stream(v.replaceAll("[ \\[\\]]", "").split(","))
|
|
||||||
.distinct()
|
|
||||||
.toList());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
PLAYER_COLUMNS.put("locale", new OptionContainer(ParkourOption.LANG, (player, v) -> {
|
|
||||||
player._locale = v;
|
|
||||||
player.locale = v;
|
|
||||||
}));
|
|
||||||
PLAYER_COLUMNS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (player, v) -> player.schematicDifficulty = Double.parseDouble(v)));
|
|
||||||
PLAYER_COLUMNS.put("sound", new OptionContainer(ParkourOption.SOUND, (player, v) -> player.sound = parseBoolean(v)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Expose Double schematicDifficulty;
|
public @Expose Double schematicDifficulty;
|
||||||
public @Expose Integer blockLead;
|
public @Expose Integer blockLead;
|
||||||
|
|
@ -95,9 +69,7 @@ public class ParkourPlayer extends ParkourUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean parseBoolean(String string) {
|
private static boolean parseBoolean(String string) {
|
||||||
return string == null
|
return string == null || string.equals("1") || string.equals("true");
|
||||||
|| string.equals("1") // for MySQL
|
|
||||||
|| string.equals("true"); // for disk
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -147,21 +119,7 @@ public class ParkourPlayer extends ParkourUser {
|
||||||
* @param settings The settings map.
|
* @param settings The settings map.
|
||||||
*/
|
*/
|
||||||
public void setSettings(@NotNull Map<String, Object> settings) {
|
public void setSettings(@NotNull Map<String, Object> settings) {
|
||||||
for (String key : PLAYER_COLUMNS.keySet()) {
|
PlayerSettingsManager.applySettings(this, settings);
|
||||||
Object value = settings.get(key);
|
|
||||||
OptionContainer container = PLAYER_COLUMNS.get(key);
|
|
||||||
|
|
||||||
if (container.consumer == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
|
|
||||||
container.consumer.accept(this, Option.OPTIONS_DEFAULTS.getOrDefault(container.option, ""));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.consumer.accept(this, String.valueOf(value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -227,7 +185,6 @@ public class ParkourPlayer extends ParkourUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Integer> getEvenlyDistributedSlots(int count) {
|
private List<Integer> getEvenlyDistributedSlots(int count) {
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,25 @@
|
||||||
package dev.loki.loparkour.player;
|
package dev.loki.loparkour.player;
|
||||||
|
|
||||||
import com.google.common.io.ByteArrayDataOutput;
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
|
||||||
import dev.loki.loparkour.api.event.ParkourJoinEvent;
|
|
||||||
import dev.loki.loparkour.api.event.ParkourLeaveEvent;
|
|
||||||
import dev.loki.loparkour.config.Config;
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.Locales;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.Option;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.hook.FloodgateHook;
|
import dev.loki.loparkour.hook.FloodgateHook;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.ParkourOption;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
|
||||||
import dev.loki.loparkour.mode.Modes;
|
|
||||||
import dev.loki.loparkour.player.data.PreviousData;
|
import dev.loki.loparkour.player.data.PreviousData;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.Session;
|
||||||
import dev.loki.loparkour.storage.Storage;
|
|
||||||
import dev.loki.loparkour.world.Divider;
|
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.util.ColorUtil;
|
||||||
import io.papermc.lib.PaperLib;
|
|
||||||
import me.clip.placeholderapi.PlaceholderAPI;
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
|
|
||||||
import org.bukkit.scoreboard.DisplaySlot;
|
import org.bukkit.scoreboard.DisplaySlot;
|
||||||
import org.bukkit.scoreboard.Objective;
|
import org.bukkit.scoreboard.Objective;
|
||||||
import org.bukkit.scoreboard.Scoreboard;
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
import org.bukkit.scoreboard.Team;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Superclass of every type of player. This encompasses every player currently in the Parkour world.
|
* Superclass of every type of player. This encompasses every player currently in the Parkour world.
|
||||||
|
|
@ -48,100 +30,31 @@ import java.util.stream.Collectors;
|
||||||
public abstract class ParkourUser {
|
public abstract class ParkourUser {
|
||||||
|
|
||||||
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
|
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
|
||||||
PreviousData data = null;
|
return UserRegistry.register(player, session);
|
||||||
ParkourUser existing = getUser(player);
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
data = existing.previousData;
|
|
||||||
unregister(existing, false, false, false);
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
ParkourPlayer pp = new ParkourPlayer(player, session, data);
|
|
||||||
|
|
||||||
joinCount++;
|
|
||||||
new ParkourJoinEvent(pp).call();
|
|
||||||
|
|
||||||
Storage.readPlayer(pp);
|
|
||||||
return pp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void leave(@NotNull Player player) {
|
public static void leave(@NotNull Player player) {
|
||||||
ParkourUser user = getUser(player);
|
UserRegistry.leave(player);
|
||||||
if (user == null) return;
|
|
||||||
leave(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void leave(@NotNull ParkourUser user) {
|
public static void leave(@NotNull ParkourUser user) {
|
||||||
unregister(user, true, true, false);
|
UserRegistry.leave(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
|
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
|
||||||
new ParkourLeaveEvent(user).call();
|
UserRegistry.unregister(user, restorePreviousData, kickIfBungee, urgent);
|
||||||
|
|
||||||
try {
|
|
||||||
user.unregister();
|
|
||||||
|
|
||||||
// Reset scoreboard to main
|
|
||||||
if (user.board != null) {
|
|
||||||
user.player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
|
||||||
user.board = null;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
|
||||||
"Error while trying to make player " + user.getName() + " leave", ex);
|
|
||||||
user.send("<red><bold>There was an error while trying to handle leaving.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restorePreviousData && Config.CONFIG.getBoolean("bungeecord.enabled") && kickIfBungee) {
|
|
||||||
sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!restorePreviousData) return;
|
|
||||||
|
|
||||||
user.previousData.apply(user.player, urgent);
|
|
||||||
|
|
||||||
Mode mode = user.session.generator.getMode();
|
|
||||||
if (mode == null) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Mode is null for %s".formatted(user.getName()));
|
|
||||||
mode = Modes.DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user instanceof ParkourPlayer player) {
|
|
||||||
Mode finalMode = mode;
|
|
||||||
user.previousData.onLeave.forEach(r -> r.execute(player, finalMode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendPlayerToServer(Player player, String server) {
|
|
||||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
|
||||||
out.writeUTF("Connect");
|
|
||||||
out.writeUTF(server);
|
|
||||||
|
|
||||||
try {
|
|
||||||
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
|
|
||||||
} catch (ChannelNotRegisteredException ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe(
|
|
||||||
"Error while trying to send %s to server %s. - %s".formatted(player.getName(), server, ex.getMessage()));
|
|
||||||
player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isUser(@Nullable Player player) {
|
public static boolean isUser(@Nullable Player player) {
|
||||||
return player != null && getUsers().stream().anyMatch(other -> other.player == player);
|
return UserRegistry.isUser(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable ParkourUser getUser(@NotNull Player player) {
|
public static @Nullable ParkourUser getUser(@NotNull Player player) {
|
||||||
return getUsers().stream()
|
return UserRegistry.getUser(player);
|
||||||
.filter(other -> other.getUUID().equals(player.getUniqueId()))
|
|
||||||
.findAny()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<ParkourUser> getUsers() {
|
public static Set<ParkourUser> getUsers() {
|
||||||
return Divider.sections.keySet().stream()
|
return UserRegistry.getUsers();
|
||||||
.flatMap(session -> session.getUsers().stream())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Fields ───────────────────────────────────────────────────────────────
|
// ─── Fields ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -153,7 +66,7 @@ public abstract class ParkourUser {
|
||||||
public final Session session;
|
public final Session session;
|
||||||
public final Player player;
|
public final Player player;
|
||||||
public final Instant joined;
|
public final Instant joined;
|
||||||
public static int joinCount;
|
private ScoreboardManager scoreboardManager;
|
||||||
|
|
||||||
public ParkourUser(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
|
public ParkourUser(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
@ -163,7 +76,8 @@ public abstract class ParkourUser {
|
||||||
|
|
||||||
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
|
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
|
||||||
this.board = Bukkit.getScoreboardManager().getNewScoreboard();
|
this.board = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||||
// Create the sidebar objective immediately and assign to player
|
this.scoreboardManager = new ScoreboardManager(player, player.getUniqueId(), board, locale);
|
||||||
|
|
||||||
Objective obj = this.board.registerNewObjective("lp_sidebar", "dummy",
|
Objective obj = this.board.registerNewObjective("lp_sidebar", "dummy",
|
||||||
ColorUtil.color(Locales.getString(locale, "scoreboard.title")));
|
ColorUtil.color(Locales.getString(locale, "scoreboard.title")));
|
||||||
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||||
|
|
@ -187,68 +101,11 @@ public abstract class ParkourUser {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the sidebar scoreboard for the given generator state.
|
* Updates the sidebar scoreboard for the given generator state.
|
||||||
* Uses vanilla Bukkit scoreboard API — Team prefix trick for coloured lines.
|
|
||||||
*/
|
*/
|
||||||
public void updateScoreboard(ParkourGenerator generator) {
|
public void updateScoreboard(ParkourGenerator generator) {
|
||||||
if (board == null || !generator.profile.get("showScoreboard").asBoolean()) {
|
if (scoreboardManager != null) {
|
||||||
return;
|
scoreboardManager.update(generator);
|
||||||
}
|
}
|
||||||
|
|
||||||
Leaderboard leaderboard = generator.getMode().getLeaderboard();
|
|
||||||
Score top = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.getScoreAtRank(1);
|
|
||||||
Score high = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.get(getUUID());
|
|
||||||
if (top == null) top = new Score("?", "?", "?", 0);
|
|
||||||
|
|
||||||
// Update title
|
|
||||||
Objective obj = board.getObjective("lp_sidebar");
|
|
||||||
if (obj == null) {
|
|
||||||
obj = board.registerNewObjective("lp_sidebar", "dummy",
|
|
||||||
ColorUtil.color(replace(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
|
||||||
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
|
||||||
} else {
|
|
||||||
obj.setDisplayName(ColorUtil.color(
|
|
||||||
replace(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update lines using Team prefix trick (each line = unique entry).
|
|
||||||
// We need globally-unique entries: use a combination of colour codes that gives
|
|
||||||
// up to 32 unique strings (§0§0, §0§1, … §1§0 …). This safely covers any
|
|
||||||
// reasonable scoreboard length.
|
|
||||||
List<String> lines = replace(Locales.getStringList(locale, "scoreboard.lines"), top, high, generator);
|
|
||||||
String[] codes = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
|
|
||||||
for (int i = 0; i < lines.size(); i++) {
|
|
||||||
String entry = "§" + codes[i / codes.length % codes.length] + "§" + codes[i % codes.length];
|
|
||||||
String teamName = "lp_line_" + i;
|
|
||||||
|
|
||||||
Team team = board.getTeam(teamName);
|
|
||||||
if (team == null) {
|
|
||||||
team = board.registerNewTeam(teamName);
|
|
||||||
team.addEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
team.setPrefix(ColorUtil.color(lines.get(i)));
|
|
||||||
obj.getScore(entry).setScore(lines.size() - i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> replace(List<String> s, Score top, Score high, ParkourGenerator generator) {
|
|
||||||
return s.stream().map(line -> replace(line, top, high, generator)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String replace(String s, Score top, Score high, ParkourGenerator generator) {
|
|
||||||
return ColorUtil.color(translate(player, s)
|
|
||||||
.replace("%score%", Integer.toString(generator.score))
|
|
||||||
.replace("%time%", generator.getFormattedTime())
|
|
||||||
.replace("%difficulty%", Double.toString(generator.getDifficultyScore()))
|
|
||||||
.replace("%top_score%", Integer.toString(top.score()))
|
|
||||||
.replace("%top_player%", top.name())
|
|
||||||
.replace("%top_time%", top.time())
|
|
||||||
.replace("%high_score%", Integer.toString(high.score()))
|
|
||||||
.replace("%high_score_time%", high.time()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String translate(Player player, String string) {
|
|
||||||
return LoParkour.getPlaceholderHook() == null ? string : PlaceholderAPI.setPlaceholders(player, string);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getUUID() { return player.getUniqueId(); }
|
public UUID getUUID() { return player.getUniqueId(); }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package dev.loki.loparkour.player;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.Option;
|
||||||
|
import dev.loki.loparkour.menu.ParkourOption;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages player settings and their mapping to database columns.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class PlayerSettingsManager {
|
||||||
|
|
||||||
|
private static final Map<String, OptionContainer> COLUMN_MAPPINGS = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
initializeColumnMappings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void initializeColumnMappings() {
|
||||||
|
COLUMN_MAPPINGS.put("uuid", new OptionContainer(null, null));
|
||||||
|
COLUMN_MAPPINGS.put("style", new OptionContainer(ParkourOption.STYLES, (p, v) -> p.style = v));
|
||||||
|
COLUMN_MAPPINGS.put("blockLead", new OptionContainer(ParkourOption.LEADS, (p, v) -> p.blockLead = Integer.parseInt(v)));
|
||||||
|
COLUMN_MAPPINGS.put("useParticles", new OptionContainer(ParkourOption.PARTICLES, (p, v) -> p.particles = parseBoolean(v)));
|
||||||
|
COLUMN_MAPPINGS.put("useSpecial", new OptionContainer(ParkourOption.SPECIAL_BLOCKS, (p, v) -> p.useSpecialBlocks = parseBoolean(v)));
|
||||||
|
COLUMN_MAPPINGS.put("showFallMsg", new OptionContainer(ParkourOption.FALL_MESSAGE, (p, v) -> p.showFallMessage = parseBoolean(v)));
|
||||||
|
COLUMN_MAPPINGS.put("showScoreboard", new OptionContainer(ParkourOption.SCOREBOARD, (p, v) -> p.showScoreboard = parseBoolean(v)));
|
||||||
|
COLUMN_MAPPINGS.put("selectedTime", new OptionContainer(ParkourOption.TIME, (p, v) -> p.selectedTime = Integer.parseInt(v)));
|
||||||
|
COLUMN_MAPPINGS.put("collectedRewards", new OptionContainer(null, PlayerSettingsManager::applyCollectedRewards));
|
||||||
|
COLUMN_MAPPINGS.put("locale", new OptionContainer(ParkourOption.LANG, (p, v) -> {
|
||||||
|
p._locale = v;
|
||||||
|
p.locale = v;
|
||||||
|
}));
|
||||||
|
COLUMN_MAPPINGS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (p, v) -> p.schematicDifficulty = Double.parseDouble(v)));
|
||||||
|
COLUMN_MAPPINGS.put("sound", new OptionContainer(ParkourOption.SOUND, (p, v) -> p.sound = parseBoolean(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, OptionContainer> getColumnMappings() {
|
||||||
|
return COLUMN_MAPPINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applySettings(ParkourPlayer player, Map<String, Object> settings) {
|
||||||
|
for (String key : COLUMN_MAPPINGS.keySet()) {
|
||||||
|
Object value = settings.get(key);
|
||||||
|
OptionContainer container = COLUMN_MAPPINGS.get(key);
|
||||||
|
|
||||||
|
if (container.consumer == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
|
||||||
|
String defaultValue = Option.OPTIONS_DEFAULTS.getOrDefault(container.option, "");
|
||||||
|
container.consumer.accept(player, defaultValue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.consumer.accept(player, String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean parseBoolean(String string) {
|
||||||
|
return string == null || string.equals("1") || string.equals("true");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyCollectedRewards(ParkourPlayer player, String value) {
|
||||||
|
player.collectedRewards = new ArrayList<>();
|
||||||
|
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
player.collectedRewards.addAll(Arrays.stream(value.replaceAll("[ \\[\\]]", "").split(","))
|
||||||
|
.distinct()
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/main/java/dev/loki/loparkour/player/ScoreboardManager.java
Normal file
132
src/main/java/dev/loki/loparkour/player/ScoreboardManager.java
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
package dev.loki.loparkour.player;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.Locales;
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
|
import dev.loki.loparkour.leaderboard.Score;
|
||||||
|
import dev.loki.loparkour.util.ColorUtil;
|
||||||
|
import me.clip.placeholderapi.PlaceholderAPI;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scoreboard.DisplaySlot;
|
||||||
|
import org.bukkit.scoreboard.Objective;
|
||||||
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
|
import org.bukkit.scoreboard.Team;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages scoreboard display for parkour users.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class ScoreboardManager {
|
||||||
|
|
||||||
|
private static final String[] COLOR_CODES = {
|
||||||
|
"0", "1", "2", "3", "4", "5", "6", "7",
|
||||||
|
"8", "9", "a", "b", "c", "d", "e", "f"
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final UUID playerUUID;
|
||||||
|
private final Scoreboard board;
|
||||||
|
private final String locale;
|
||||||
|
|
||||||
|
public ScoreboardManager(Player player, UUID playerUUID, Scoreboard board, String locale) {
|
||||||
|
this.player = player;
|
||||||
|
this.playerUUID = playerUUID;
|
||||||
|
this.board = board;
|
||||||
|
this.locale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(ParkourGenerator generator) {
|
||||||
|
if (board == null || !generator.profile.get("showScoreboard").asBoolean()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Leaderboard leaderboard = generator.getMode().getLeaderboard();
|
||||||
|
Score top = getTopScore(leaderboard);
|
||||||
|
Score high = getHighScore(leaderboard);
|
||||||
|
|
||||||
|
updateTitle(top, high, generator);
|
||||||
|
updateLines(top, high, generator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Score getTopScore(Leaderboard leaderboard) {
|
||||||
|
if (leaderboard == null) return createDefaultScore();
|
||||||
|
Score score = leaderboard.getScoreAtRank(1);
|
||||||
|
return score != null ? score : createDefaultScore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Score getHighScore(Leaderboard leaderboard) {
|
||||||
|
if (leaderboard == null) return createDefaultScore();
|
||||||
|
Score score = leaderboard.get(playerUUID);
|
||||||
|
return score != null ? score : createDefaultScore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Score createDefaultScore() {
|
||||||
|
return new Score("?", "?", "?", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTitle(Score top, Score high, ParkourGenerator generator) {
|
||||||
|
Objective obj = board.getObjective("lp_sidebar");
|
||||||
|
if (obj == null) {
|
||||||
|
obj = board.registerNewObjective("lp_sidebar", "dummy",
|
||||||
|
ColorUtil.color(replacePlaceholders(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
||||||
|
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||||
|
} else {
|
||||||
|
obj.setDisplayName(ColorUtil.color(
|
||||||
|
replacePlaceholders(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLines(Score top, Score high, ParkourGenerator generator) {
|
||||||
|
Objective obj = board.getObjective("lp_sidebar");
|
||||||
|
if (obj == null) return;
|
||||||
|
|
||||||
|
List<String> lines = Locales.getStringList(locale, "scoreboard.lines").stream()
|
||||||
|
.map(line -> replacePlaceholders(line, top, high, generator))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
String entry = createUniqueEntry(i);
|
||||||
|
String teamName = "lp_line_" + i;
|
||||||
|
|
||||||
|
Team team = board.getTeam(teamName);
|
||||||
|
if (team == null) {
|
||||||
|
team = board.registerNewTeam(teamName);
|
||||||
|
team.addEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
team.setPrefix(ColorUtil.color(lines.get(i)));
|
||||||
|
obj.getScore(entry).setScore(lines.size() - i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createUniqueEntry(int index) {
|
||||||
|
return "§" + COLOR_CODES[index / COLOR_CODES.length % COLOR_CODES.length] +
|
||||||
|
"§" + COLOR_CODES[index % COLOR_CODES.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
private String replacePlaceholders(String text, Score top, Score high, ParkourGenerator generator) {
|
||||||
|
String replaced = text
|
||||||
|
.replace("%score%", Integer.toString(generator.state.score))
|
||||||
|
.replace("%time%", generator.getFormattedTime())
|
||||||
|
.replace("%difficulty%", Double.toString(generator.getDifficultyScore()))
|
||||||
|
.replace("%top_score%", Integer.toString(top.score()))
|
||||||
|
.replace("%top_player%", top.name())
|
||||||
|
.replace("%top_time%", top.time())
|
||||||
|
.replace("%high_score%", Integer.toString(high.score()))
|
||||||
|
.replace("%high_score_time%", high.time());
|
||||||
|
|
||||||
|
return applyPlaceholderAPI(replaced);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String applyPlaceholderAPI(String text) {
|
||||||
|
try {
|
||||||
|
return PlaceholderAPI.setPlaceholders(player, text);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/main/java/dev/loki/loparkour/player/UserRegistry.java
Normal file
128
src/main/java/dev/loki/loparkour/player/UserRegistry.java
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
package dev.loki.loparkour.player;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.api.event.ParkourJoinEvent;
|
||||||
|
import dev.loki.loparkour.api.event.ParkourLeaveEvent;
|
||||||
|
import dev.loki.loparkour.config.Config;
|
||||||
|
import dev.loki.loparkour.mode.Mode;
|
||||||
|
import dev.loki.loparkour.mode.Modes;
|
||||||
|
import dev.loki.loparkour.player.data.PreviousData;
|
||||||
|
import dev.loki.loparkour.session.Session;
|
||||||
|
import dev.loki.loparkour.storage.Storage;
|
||||||
|
import dev.loki.loparkour.world.Divider;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages registration and unregistration of parkour users.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
public class UserRegistry {
|
||||||
|
|
||||||
|
private static int joinCount = 0;
|
||||||
|
|
||||||
|
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
|
||||||
|
PreviousData data = null;
|
||||||
|
ParkourUser existing = getUser(player);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
data = existing.previousData;
|
||||||
|
unregister(existing, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ParkourPlayer pp = new ParkourPlayer(player, session, data);
|
||||||
|
joinCount++;
|
||||||
|
new ParkourJoinEvent(pp).call();
|
||||||
|
Storage.readPlayer(pp);
|
||||||
|
|
||||||
|
return pp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void leave(@NotNull Player player) {
|
||||||
|
ParkourUser user = getUser(player);
|
||||||
|
if (user != null) {
|
||||||
|
leave(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void leave(@NotNull ParkourUser user) {
|
||||||
|
unregister(user, true, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
|
||||||
|
new ParkourLeaveEvent(user).call();
|
||||||
|
|
||||||
|
try {
|
||||||
|
user.unregister();
|
||||||
|
resetScoreboard(user);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleUnregisterError(user, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restorePreviousData && shouldKickToBungee(kickIfBungee)) {
|
||||||
|
BungeeUtil.sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restorePreviousData) {
|
||||||
|
restorePreviousState(user, urgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isUser(@Nullable Player player) {
|
||||||
|
return player != null && getUsers().stream().anyMatch(other -> other.player == player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable ParkourUser getUser(@NotNull Player player) {
|
||||||
|
return getUsers().stream()
|
||||||
|
.filter(other -> other.getUUID().equals(player.getUniqueId()))
|
||||||
|
.findAny()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<ParkourUser> getUsers() {
|
||||||
|
return Divider.sections.keySet().stream()
|
||||||
|
.flatMap(session -> session.getUsers().stream())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getJoinCount() {
|
||||||
|
return joinCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resetScoreboard(ParkourUser user) {
|
||||||
|
if (user.board != null) {
|
||||||
|
user.player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||||
|
user.board = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleUnregisterError(ParkourUser user, Exception ex) {
|
||||||
|
user.player.getServer().getLogger().log(java.util.logging.Level.SEVERE,
|
||||||
|
"Error while trying to make player " + user.getName() + " leave", ex);
|
||||||
|
user.send("<red><bold>There was an error while trying to handle leaving.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldKickToBungee(boolean kickIfBungee) {
|
||||||
|
return Config.CONFIG.getBoolean("bungeecord.enabled") && kickIfBungee;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void restorePreviousState(ParkourUser user, boolean urgent) {
|
||||||
|
user.previousData.apply(user.player, urgent);
|
||||||
|
|
||||||
|
Mode mode = user.session.generator.getMode();
|
||||||
|
if (mode == null) {
|
||||||
|
mode = Modes.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user instanceof ParkourPlayer player) {
|
||||||
|
Mode finalMode = mode;
|
||||||
|
user.previousData.onLeave.forEach(r -> r.execute(player, finalMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,14 +42,9 @@ public class Session {
|
||||||
private Visibility visibility = Visibility.PUBLIC;
|
private Visibility visibility = Visibility.PUBLIC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of muted users.
|
* Manages users (players and spectators) in this session.
|
||||||
*/
|
*/
|
||||||
private final List<ParkourUser> muted = new ArrayList<>();
|
private final SessionUserManager userManager = new SessionUserManager(this);
|
||||||
|
|
||||||
/**
|
|
||||||
* List of users.
|
|
||||||
*/
|
|
||||||
private final Map<UUID, ParkourUser> users = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that takes the current session and returns whether new players should be accepted.
|
* Function that takes the current session and returns whether new players should be accepted.
|
||||||
|
|
@ -121,13 +116,7 @@ public class Session {
|
||||||
* @param toAdd The players to add.
|
* @param toAdd The players to add.
|
||||||
*/
|
*/
|
||||||
public void addPlayers(ParkourPlayer... toAdd) {
|
public void addPlayers(ParkourPlayer... toAdd) {
|
||||||
for (ParkourPlayer player : toAdd) {
|
userManager.addPlayers(toAdd);
|
||||||
for (ParkourPlayer to : getPlayers()) {
|
|
||||||
to.send(Locales.getString(player.locale, "lobby.other_join").formatted(player.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
users.put(player.getUUID(), player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -136,31 +125,14 @@ public class Session {
|
||||||
* @param toRemove The players to remove.
|
* @param toRemove The players to remove.
|
||||||
*/
|
*/
|
||||||
public void removePlayers(ParkourPlayer... toRemove) {
|
public void removePlayers(ParkourPlayer... toRemove) {
|
||||||
for (ParkourPlayer player : toRemove) {
|
userManager.removePlayers(toRemove);
|
||||||
users.remove(player.getUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ParkourPlayer> players = getPlayers();
|
|
||||||
for (ParkourPlayer player : toRemove) {
|
|
||||||
for (ParkourPlayer to : players) {
|
|
||||||
to.send(Locales.getString(player.locale, "lobby.other_leave").formatted(player.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toRemove.length > 0 && players.isEmpty()) {
|
|
||||||
generator.reset(false);
|
|
||||||
Divider.remove(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The players.
|
* @return The players.
|
||||||
*/
|
*/
|
||||||
public List<ParkourPlayer> getPlayers() {
|
public List<ParkourPlayer> getPlayers() {
|
||||||
return users.values().stream()
|
return userManager.getPlayers();
|
||||||
.filter(user -> user instanceof ParkourPlayer)
|
|
||||||
.map(user -> (ParkourPlayer) user)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -169,13 +141,7 @@ public class Session {
|
||||||
* @param spectators The spectators to add.
|
* @param spectators The spectators to add.
|
||||||
*/
|
*/
|
||||||
public void addSpectators(ParkourSpectator... spectators) {
|
public void addSpectators(ParkourSpectator... spectators) {
|
||||||
for (ParkourSpectator spectator : spectators) {
|
userManager.addSpectators(spectators);
|
||||||
for (ParkourPlayer player : getPlayers()) {
|
|
||||||
player.sendTranslated("play.spectator.other_join", spectator.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
users.put(spectator.getUUID(), spectator);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -184,30 +150,21 @@ public class Session {
|
||||||
* @param spectators The spectators to remove.
|
* @param spectators The spectators to remove.
|
||||||
*/
|
*/
|
||||||
public void removeSpectators(ParkourSpectator... spectators) {
|
public void removeSpectators(ParkourSpectator... spectators) {
|
||||||
for (ParkourSpectator spectator : spectators) {
|
userManager.removeSpectators(spectators);
|
||||||
for (ParkourPlayer player : getPlayers()) {
|
|
||||||
player.sendTranslated("play.spectator.other_leave", spectator.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
users.remove(spectator.getUUID());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The spectators.
|
* @return The spectators.
|
||||||
*/
|
*/
|
||||||
public List<ParkourSpectator> getSpectators() {
|
public List<ParkourSpectator> getSpectators() {
|
||||||
return users.values().stream()
|
return userManager.getSpectators();
|
||||||
.filter(user -> user instanceof ParkourSpectator)
|
|
||||||
.map(user -> (ParkourSpectator) user)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The users.
|
* @return The users.
|
||||||
*/
|
*/
|
||||||
public List<ParkourUser> getUsers() {
|
public List<ParkourUser> getUsers() {
|
||||||
return new ArrayList<>(users.values());
|
return userManager.getUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -224,9 +181,7 @@ public class Session {
|
||||||
* @param user The user to (un)mute.
|
* @param user The user to (un)mute.
|
||||||
*/
|
*/
|
||||||
public void toggleMute(@NotNull ParkourUser user) {
|
public void toggleMute(@NotNull ParkourUser user) {
|
||||||
if (!muted.remove(user)) {
|
userManager.toggleMute(user);
|
||||||
muted.add(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -234,7 +189,15 @@ public class Session {
|
||||||
* @return True when the user is muted, false if not.
|
* @return True when the user is muted, false if not.
|
||||||
*/
|
*/
|
||||||
public boolean isMuted(@NotNull ParkourUser user) {
|
public boolean isMuted(@NotNull ParkourUser user) {
|
||||||
return muted.contains(user);
|
return userManager.isMuted(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when all players have left the session.
|
||||||
|
*/
|
||||||
|
void onAllPlayersLeft() {
|
||||||
|
generator.reset(false);
|
||||||
|
Divider.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
112
src/main/java/dev/loki/loparkour/session/SessionUserManager.java
Normal file
112
src/main/java/dev/loki/loparkour/session/SessionUserManager.java
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package dev.loki.loparkour.session;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.Locales;
|
||||||
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.player.ParkourSpectator;
|
||||||
|
import dev.loki.loparkour.player.ParkourUser;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages users (players and spectators) within a session.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class SessionUserManager {
|
||||||
|
|
||||||
|
private final Map<UUID, ParkourUser> users = new HashMap<>();
|
||||||
|
private final List<ParkourUser> muted = new ArrayList<>();
|
||||||
|
private final Session session;
|
||||||
|
|
||||||
|
public SessionUserManager(Session session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPlayers(ParkourPlayer... toAdd) {
|
||||||
|
for (ParkourPlayer player : toAdd) {
|
||||||
|
notifyJoin(player);
|
||||||
|
users.put(player.getUUID(), player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePlayers(ParkourPlayer... toRemove) {
|
||||||
|
for (ParkourPlayer player : toRemove) {
|
||||||
|
users.remove(player.getUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ParkourPlayer> remainingPlayers = getPlayers();
|
||||||
|
notifyLeave(toRemove, remainingPlayers);
|
||||||
|
|
||||||
|
if (toRemove.length > 0 && remainingPlayers.isEmpty()) {
|
||||||
|
session.onAllPlayersLeft();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSpectators(ParkourSpectator... spectators) {
|
||||||
|
for (ParkourSpectator spectator : spectators) {
|
||||||
|
notifySpectatorJoin(spectator);
|
||||||
|
users.put(spectator.getUUID(), spectator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSpectators(ParkourSpectator... spectators) {
|
||||||
|
for (ParkourSpectator spectator : spectators) {
|
||||||
|
notifySpectatorLeave(spectator);
|
||||||
|
users.remove(spectator.getUUID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ParkourPlayer> getPlayers() {
|
||||||
|
return users.values().stream()
|
||||||
|
.filter(user -> user instanceof ParkourPlayer)
|
||||||
|
.map(user -> (ParkourPlayer) user)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ParkourSpectator> getSpectators() {
|
||||||
|
return users.values().stream()
|
||||||
|
.filter(user -> user instanceof ParkourSpectator)
|
||||||
|
.map(user -> (ParkourSpectator) user)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ParkourUser> getUsers() {
|
||||||
|
return new ArrayList<>(users.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleMute(ParkourUser user) {
|
||||||
|
if (!muted.remove(user)) {
|
||||||
|
muted.add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMuted(ParkourUser user) {
|
||||||
|
return muted.contains(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyJoin(ParkourPlayer player) {
|
||||||
|
for (ParkourPlayer to : getPlayers()) {
|
||||||
|
to.send(Locales.getString(player.locale, "lobby.other_join").formatted(player.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyLeave(ParkourPlayer[] toRemove, List<ParkourPlayer> remainingPlayers) {
|
||||||
|
for (ParkourPlayer player : toRemove) {
|
||||||
|
for (ParkourPlayer to : remainingPlayers) {
|
||||||
|
to.send(Locales.getString(player.locale, "lobby.other_leave").formatted(player.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySpectatorJoin(ParkourSpectator spectator) {
|
||||||
|
for (ParkourPlayer player : getPlayers()) {
|
||||||
|
player.sendTranslated("play.spectator.other_join", spectator.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySpectatorLeave(ParkourSpectator spectator) {
|
||||||
|
for (ParkourPlayer player : getPlayers()) {
|
||||||
|
player.sendTranslated("play.spectator.other_leave", spectator.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package dev.loki.loparkour.storage;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import dev.loki.loparkour.config.Option;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages SQL database connection lifecycle.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class SQLConnectionManager {
|
||||||
|
|
||||||
|
private Connection connection;
|
||||||
|
private volatile boolean connected = false;
|
||||||
|
private final List<Runnable> onConnectCallbacks = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runWhenConnected(Runnable callback) {
|
||||||
|
if (connected) {
|
||||||
|
callback.run();
|
||||||
|
} else {
|
||||||
|
onConnectCallbacks.add(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect() {
|
||||||
|
try {
|
||||||
|
LoParkour.log("Connecting to MySQL...");
|
||||||
|
loadDriver();
|
||||||
|
|
||||||
|
connection = DriverManager.getConnection(
|
||||||
|
buildConnectionUrl(),
|
||||||
|
Option.SQL_USERNAME,
|
||||||
|
Option.SQL_PASSWORD);
|
||||||
|
|
||||||
|
connected = true;
|
||||||
|
LoParkour.log("Connected to MySQL");
|
||||||
|
|
||||||
|
executeCallbacks();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleConnectionError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (connection != null && !connection.isClosed()) {
|
||||||
|
connection.close();
|
||||||
|
LoParkour.log("Closed connection to MySQL");
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error while trying to close connection to SQL database - " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedStatement prepareStatement(String sql) {
|
||||||
|
validateConnection();
|
||||||
|
if (connection == null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return connection.prepareStatement(sql);
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error preparing statement: %s - %s".formatted(sql, ex.getMessage()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateConnection() {
|
||||||
|
try {
|
||||||
|
if (connection == null || !connection.isValid(2)) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning("MySQL connection lost, attempting reconnect...");
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe("Error reconnecting to MySQL - " + ex.getMessage());
|
||||||
|
Option.SQL = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadDriver() throws ClassNotFoundException {
|
||||||
|
try {
|
||||||
|
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
Class.forName("com.mysql.jdbc.Driver");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildConnectionUrl() {
|
||||||
|
return ("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
|
||||||
|
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
|
||||||
|
"&autoReconnect=true&maxReconnects=2&connectTimeout=5000&socketTimeout=5000")
|
||||||
|
.formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeCallbacks() {
|
||||||
|
for (Runnable cb : onConnectCallbacks) {
|
||||||
|
try {
|
||||||
|
cb.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onConnectCallbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleConnectionError(Exception ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Could not connect to MySQL - check your SQL settings - " + ex.getMessage());
|
||||||
|
LoParkour.getPlugin().getLogger().severe("Disabling SQL storage, using local storage instead");
|
||||||
|
Option.SQL = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package dev.loki.loparkour.storage;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.Option;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages database schema migrations.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class SQLMigrationManager {
|
||||||
|
|
||||||
|
private final SQLQueryExecutor queryExecutor;
|
||||||
|
|
||||||
|
public SQLMigrationManager(SQLQueryExecutor queryExecutor) {
|
||||||
|
this.queryExecutor = queryExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeDatabase() {
|
||||||
|
createDatabase();
|
||||||
|
createOptionsTable();
|
||||||
|
runMigrations();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDatabase() {
|
||||||
|
queryExecutor.executeUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
|
||||||
|
queryExecutor.executeUpdate("USE `%s`;".formatted(Option.SQL_DB));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createOptionsTable() {
|
||||||
|
queryExecutor.executeUpdate(
|
||||||
|
("CREATE TABLE IF NOT EXISTS `%soptions` " +
|
||||||
|
"(`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, " +
|
||||||
|
"`useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, " +
|
||||||
|
"`showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) " +
|
||||||
|
"ENGINE = InnoDB CHARSET = utf8;").formatted(Option.SQL_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runMigrations() {
|
||||||
|
String prefix = Option.SQL_PREFIX;
|
||||||
|
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `collectedRewards` MEDIUMTEXT;".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `locale` VARCHAR(8);".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `schematicDifficulty` DOUBLE;".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `sound` BOOLEAN;".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(prefix));
|
||||||
|
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(prefix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package dev.loki.loparkour.storage;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes SQL queries with error handling.
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class SQLQueryExecutor {
|
||||||
|
|
||||||
|
private final SQLConnectionManager connectionManager;
|
||||||
|
|
||||||
|
public SQLQueryExecutor(SQLConnectionManager connectionManager) {
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void executeUpdate(String sql) {
|
||||||
|
executeUpdate(sql, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void executeUpdateSuppressed(String sql) {
|
||||||
|
executeUpdate(sql, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeUpdate(String sql, boolean suppressErrors) {
|
||||||
|
connectionManager.validateConnection();
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connectionManager.prepareStatement(sql)) {
|
||||||
|
if (stmt != null) {
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
if (!suppressErrors) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error while sending update: %s - %s".formatted(sql, ex.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedStatement prepareStatement(String sql) {
|
||||||
|
return connectionManager.prepareStatement(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,32 +21,19 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
*/
|
*/
|
||||||
class StorageSQL {
|
class StorageSQL {
|
||||||
|
|
||||||
private static Connection connection;
|
private static final SQLConnectionManager connectionManager = new SQLConnectionManager();
|
||||||
private static boolean initialized = false;
|
private static final SQLQueryExecutor queryExecutor = new SQLQueryExecutor(connectionManager);
|
||||||
private static volatile boolean connected = false;
|
private static final SQLMigrationManager migrationManager = new SQLMigrationManager(queryExecutor);
|
||||||
|
|
||||||
// Tables queued to create after connection is established
|
private static boolean initialized = false;
|
||||||
private static final List<String> pendingTableCreations = new CopyOnWriteArrayList<>();
|
private static final List<String> pendingTableCreations = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
// Callbacks to run after connection is ready (e.g. initial leaderboard reads)
|
|
||||||
private static final List<Runnable> onConnectCallbacks = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the SQL connection is ready.
|
|
||||||
*/
|
|
||||||
public static boolean isConnected() {
|
public static boolean isConnected() {
|
||||||
return connected;
|
return connectionManager.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the given callback immediately if already connected, or defers it until connect() succeeds.
|
|
||||||
*/
|
|
||||||
public static void runWhenConnected(Runnable callback) {
|
public static void runWhenConnected(Runnable callback) {
|
||||||
if (connected) {
|
connectionManager.runWhenConnected(callback);
|
||||||
callback.run();
|
|
||||||
} else {
|
|
||||||
onConnectCallbacks.add(callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(String mode) {
|
public static void init(String mode) {
|
||||||
|
|
@ -64,30 +51,20 @@ class StorageSQL {
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
// Queue this table for creation after connect() finishes
|
|
||||||
pendingTableCreations.add(createTableSql);
|
pendingTableCreations.add(createTableSql);
|
||||||
// Connect async — table creation happens inside connect() after connection is ready
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(LoParkour.getPlugin(), StorageSQL::connect);
|
Bukkit.getScheduler().runTaskAsynchronously(LoParkour.getPlugin(), StorageSQL::connect);
|
||||||
} else {
|
} else {
|
||||||
// Already connected (or connecting) — send directly
|
|
||||||
pendingTableCreations.add(createTableSql);
|
pendingTableCreations.add(createTableSql);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void close() {
|
public static void close() {
|
||||||
try {
|
connectionManager.close();
|
||||||
if (connection != null && !connection.isClosed()) {
|
|
||||||
connection.close();
|
|
||||||
LoParkour.log("Closed connection to MySQL");
|
|
||||||
}
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Error while trying to close connection to SQL database - " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
||||||
String sql = "SELECT * FROM `%s`;".formatted(getTableName(mode));
|
String sql = "SELECT * FROM `%s`;".formatted(getTableName(mode));
|
||||||
try (PreparedStatement stmt = prepareStatement(sql)) {
|
try (PreparedStatement stmt = queryExecutor.prepareStatement(sql)) {
|
||||||
if (stmt == null) return new HashMap<>();
|
if (stmt == null) return new HashMap<>();
|
||||||
try (ResultSet results = stmt.executeQuery()) {
|
try (ResultSet results = stmt.executeQuery()) {
|
||||||
Map<UUID, Score> scores = new HashMap<>();
|
Map<UUID, Score> scores = new HashMap<>();
|
||||||
|
|
@ -101,13 +78,14 @@ class StorageSQL {
|
||||||
return scores;
|
return scores;
|
||||||
}
|
}
|
||||||
} catch (SQLException ex) {
|
} catch (SQLException ex) {
|
||||||
LoParkour.getPlugin().getLogger().severe("Error while trying to read SQL data of %s - %s".formatted(mode, ex.getMessage()));
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error while trying to read SQL data of %s - %s".formatted(mode, ex.getMessage()));
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||||
new HashMap<>(scores).forEach((uuid, score) -> sendUpdate("""
|
new HashMap<>(scores).forEach((uuid, score) -> queryExecutor.executeUpdate("""
|
||||||
INSERT INTO `%s`
|
INSERT INTO `%s`
|
||||||
(uuid, name, time, difficulty, score)
|
(uuid, name, time, difficulty, score)
|
||||||
VALUES ('%s', '%s', '%s', '%s', %d)
|
VALUES ('%s', '%s', '%s', '%s', %d)
|
||||||
|
|
@ -125,10 +103,16 @@ class StorageSQL {
|
||||||
|
|
||||||
public static void readPlayer(@NotNull ParkourPlayer player) {
|
public static void readPlayer(@NotNull ParkourPlayer player) {
|
||||||
String sql = "SELECT * FROM `%soptions` WHERE uuid = '%s';".formatted(Option.SQL_PREFIX, player.getUUID());
|
String sql = "SELECT * FROM `%soptions` WHERE uuid = '%s';".formatted(Option.SQL_PREFIX, player.getUUID());
|
||||||
try (PreparedStatement stmt = prepareStatement(sql)) {
|
try (PreparedStatement stmt = queryExecutor.prepareStatement(sql)) {
|
||||||
if (stmt == null) { player.setSettings(new HashMap<>()); return; }
|
if (stmt == null) {
|
||||||
|
player.setSettings(new HashMap<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
try (ResultSet results = stmt.executeQuery()) {
|
try (ResultSet results = stmt.executeQuery()) {
|
||||||
if (!results.next()) { player.setSettings(new HashMap<>()); return; }
|
if (!results.next()) {
|
||||||
|
player.setSettings(new HashMap<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> settings = new HashMap<>();
|
Map<String, Object> settings = new HashMap<>();
|
||||||
for (String key : ParkourPlayer.PLAYER_COLUMNS.keySet()) {
|
for (String key : ParkourPlayer.PLAYER_COLUMNS.keySet()) {
|
||||||
|
|
@ -142,7 +126,8 @@ class StorageSQL {
|
||||||
player.setSettings(settings);
|
player.setSettings(settings);
|
||||||
}
|
}
|
||||||
} catch (SQLException ex) {
|
} catch (SQLException ex) {
|
||||||
LoParkour.getPlugin().getLogger().severe("Error reading SQL data of %s - %s".formatted(player.getName(), ex.getMessage()));
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error reading SQL data of %s - %s".formatted(player.getName(), ex.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,7 +135,7 @@ class StorageSQL {
|
||||||
DecimalFormat df = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
DecimalFormat df = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||||
String schematicDifficulty = df.format(player.schematicDifficulty);
|
String schematicDifficulty = df.format(player.schematicDifficulty);
|
||||||
|
|
||||||
sendUpdate("""
|
queryExecutor.executeUpdate("""
|
||||||
INSERT INTO `%soptions`
|
INSERT INTO `%soptions`
|
||||||
(uuid, style, blockLead, useParticles, useSpecial, showFallMsg, showScoreboard,
|
(uuid, style, blockLead, useParticles, useSpecial, showFallMsg, showScoreboard,
|
||||||
selectedTime, collectedRewards, locale, schematicDifficulty, sound)
|
selectedTime, collectedRewards, locale, schematicDifficulty, sound)
|
||||||
|
|
@ -178,105 +163,12 @@ class StorageSQL {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void connect() {
|
private static void connect() {
|
||||||
try {
|
connectionManager.connect();
|
||||||
LoParkour.log("Connecting to MySQL...");
|
migrationManager.initializeDatabase();
|
||||||
|
|
||||||
try {
|
for (String sql : pendingTableCreations) {
|
||||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
queryExecutor.executeUpdate(sql);
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
Class.forName("com.mysql.jdbc.Driver");
|
|
||||||
}
|
|
||||||
|
|
||||||
connection = DriverManager.getConnection(
|
|
||||||
("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
|
|
||||||
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
|
|
||||||
"&autoReconnect=true&maxReconnects=2&connectTimeout=5000&socketTimeout=5000")
|
|
||||||
.formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB),
|
|
||||||
Option.SQL_USERNAME, Option.SQL_PASSWORD);
|
|
||||||
|
|
||||||
sendUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
|
|
||||||
sendUpdate("USE `%s`;".formatted(Option.SQL_DB));
|
|
||||||
|
|
||||||
// Base options table
|
|
||||||
sendUpdate(("CREATE TABLE IF NOT EXISTS `%soptions` " +
|
|
||||||
"(`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, " +
|
|
||||||
"`useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, " +
|
|
||||||
"`showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) " +
|
|
||||||
"ENGINE = InnoDB CHARSET = utf8;").formatted(Option.SQL_PREFIX));
|
|
||||||
|
|
||||||
// Migrations
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `collectedRewards` MEDIUMTEXT;".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `locale` VARCHAR(8);".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `schematicDifficulty` DOUBLE;".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `sound` BOOLEAN;".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(Option.SQL_PREFIX));
|
|
||||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(Option.SQL_PREFIX));
|
|
||||||
|
|
||||||
// Now flush all pending table creations (leaderboard tables queued before connection was ready)
|
|
||||||
for (String sql : pendingTableCreations) {
|
|
||||||
sendUpdate(sql);
|
|
||||||
}
|
|
||||||
pendingTableCreations.clear();
|
|
||||||
|
|
||||||
LoParkour.log("Connected to MySQL");
|
|
||||||
connected = true;
|
|
||||||
|
|
||||||
// Run deferred callbacks (e.g. initial leaderboard reads)
|
|
||||||
for (Runnable cb : onConnectCallbacks) {
|
|
||||||
try { cb.run(); } catch (Exception e) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onConnectCallbacks.clear();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Could not connect to MySQL - check your SQL settings - " + ex.getMessage());
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Disabling SQL storage, using local storage instead");
|
|
||||||
Option.SQL = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateConnection() {
|
|
||||||
try {
|
|
||||||
if (connection == null || !connection.isValid(2)) {
|
|
||||||
LoParkour.getPlugin().getLogger().warning("MySQL connection lost, attempting reconnect...");
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Error reconnecting to MySQL - " + ex.getMessage());
|
|
||||||
Option.SQL = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a PreparedStatement with proper connection validation. Caller must close it. */
|
|
||||||
private static PreparedStatement prepareStatement(String sql) {
|
|
||||||
validateConnection();
|
|
||||||
if (connection == null) return null;
|
|
||||||
try {
|
|
||||||
return connection.prepareStatement(sql);
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Error preparing statement: %s - %s".formatted(sql, ex.getMessage()));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendUpdate(String sql) {
|
|
||||||
validateConnection();
|
|
||||||
if (connection == null) return;
|
|
||||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
|
||||||
stmt.executeUpdate();
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe("Error while sending update: %s - %s".formatted(sql, ex.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendUpdateSuppressed(String sql) {
|
|
||||||
validateConnection();
|
|
||||||
if (connection == null) return;
|
|
||||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
|
||||||
stmt.executeUpdate();
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
}
|
||||||
|
pendingTableCreations.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue