Folder refactoring fixed the broken file
This commit is contained in:
parent
574f9fc4b5
commit
261c4a5cab
24 changed files with 385 additions and 150 deletions
|
|
@ -1,91 +0,0 @@
|
||||||
# Рефакторинг - Краткое описание
|
|
||||||
|
|
||||||
## Выполненные изменения
|
|
||||||
|
|
||||||
### 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 остались без изменений. Рефакторинг затронул только внутреннюю структуру классов.
|
|
||||||
64
TODO.md
64
TODO.md
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
| # | Баг | Файл | Что сделать |
|
| # | Баг | Файл | Что сделать |
|
||||||
|---|-----|------|-------------|
|
|---|-----|------|-------------|
|
||||||
|
| ✅ | ~~BOM в `ParkourGenerator.java` ломает компиляцию~~ | `ParkourGenerator.java` | ✅ Удалён через Python |
|
||||||
|
| ✅ | ~~`generator.score` → `generator.state.score`~~ | `PAPIHook.java`, `ParkourSettingsMenu.java` | ✅ Исправлено |
|
||||||
|
| ✅ | ~~`ParkourUser.joinCount` удалён~~ | `LoParkour.java` | ✅ Использован `UserRegistry.getJoinCount()` |
|
||||||
|
| ✅ | ~~`PlayerSettingsManager.OptionContainer` не public~~ | `PlayerSettingsManager.java` | ✅ Сделан public |
|
||||||
|
| ✅ | ~~`Option.PARTICLE_DATA.data()` не существует~~ | `EffectManager.java` | ✅ Убран `.data()` |
|
||||||
|
| ✅ | ~~`ParticleUtil.circle()` неправильные параметры~~ | `EffectManager.java` | ✅ Исправлено на (Location, Particle, int, int) |
|
||||||
| 1 | `PotionEffectType.JUMP` и `SLOW` удалены в Paper 1.20.5+ | `GravityShiftMode.java` | Заменить на `JUMP_BOOST` и `SLOWNESS` |
|
| 1 | `PotionEffectType.JUMP` и `SLOW` удалены в Paper 1.20.5+ | `GravityShiftMode.java` | Заменить на `JUMP_BOOST` и `SLOWNESS` |
|
||||||
| 2 | `ElytraMode` создаёт обычный `ParkourGenerator` → `getMode()` возвращает DEFAULT | `ElytraMode.java` | Создать `ElytraGenerator extends ParkourGenerator` |
|
| 2 | `ElytraMode` создаёт обычный `ParkourGenerator` → `getMode()` возвращает DEFAULT | `ElytraMode.java` | Создать `ElytraGenerator extends ParkourGenerator` |
|
||||||
| 3 | `SpeedrunMode`: таймеры `runLater` продолжают тикать после `reset()` — гонка с генератором | `SpeedrunMode.java` | Хранить `ScheduledTask`, отменять в `reset()` |
|
| 3 | `SpeedrunMode`: таймеры `runLater` продолжают тикать после `reset()` — гонка с генератором | `SpeedrunMode.java` | Хранить `ScheduledTask`, отменять в `reset()` |
|
||||||
|
|
@ -27,21 +33,72 @@
|
||||||
| 4 | `HardcoreMode`: `collectedRewards` не очищается при падении | `HardcoreMode.java` | Добавить `player.collectedRewards.clear()` в `fall()` |
|
| 4 | `HardcoreMode`: `collectedRewards` не очищается при падении | `HardcoreMode.java` | Добавить `player.collectedRewards.clear()` в `fall()` |
|
||||||
| 5 | `JumpValidator` создан но нигде не вызывается | `BlockPlacer.java` | Добавить retry-loop в `selectNext()` (до 10 попыток) |
|
| 5 | `JumpValidator` создан но нигде не вызывается | `BlockPlacer.java` | Добавить retry-loop в `selectNext()` (до 10 попыток) |
|
||||||
| 6 | `JumpType` enum создан но не интегрирован в генератор | `BlockPlacer.java` | Добавить выбор JumpType по шансу из конфига |
|
| 6 | `JumpType` enum создан но не интегрирован в генератор | `BlockPlacer.java` | Добавить выбор JumpType по шансу из конфига |
|
||||||
| 7 | `ConfigUpdater` закомментирован | `Config.java` | Раскомментировать, проверить `ignoredSections` |
|
| 7 | ~~`ConfigUpdater` закомментирован~~ | `Config.java` | ✅ Написан `ConfigUpdater.java`, подключён в `Config.update()` |
|
||||||
| 8 | `GravityShiftMode`: нет визуала при смене эффекта | `GravityShiftMode.java` | Добавить Title + Sound + партиклы `PORTAL` |
|
| 8 | `GravityShiftMode`: нет визуала при смене эффекта | `GravityShiftMode.java` | Добавить Title + Sound + партиклы `PORTAL` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🟢 Планово — дуэли
|
||||||
|
|
||||||
|
### DuelMode
|
||||||
|
- [ ] `DuelMode implements Mode` — режим дуэли
|
||||||
|
- [ ] `DuelSession extends Session` — два игрока, отдельные острова
|
||||||
|
- [ ] `/lp duel <игрок>` — вызов на дуэль, запрос истекает через 30 сек
|
||||||
|
- [ ] Форматы: **Race** (кто первый до N очков), **Survival** (3 жизни), **Time Attack** (60 сек кто больше)
|
||||||
|
- [ ] Scoreboard показывает счёт обоих в реальном времени
|
||||||
|
- [ ] ActionBar: `Opponent: 34 pts` во время игры
|
||||||
|
- [ ] При падении противника — уведомление
|
||||||
|
- [ ] Победитель получает награды через систему `Rewards`
|
||||||
|
- [ ] Лидерборд дуэлей (W/L)
|
||||||
|
- [ ] Кнопка реванша после окончания
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🟢 Планово
|
## 🟢 Планово
|
||||||
|
|
||||||
| # | Фича | Файл | Что сделать |
|
| # | Фича | Файл | Что сделать |
|
||||||
|---|------|------|-------------|
|
|---|------|------|-------------|
|
||||||
| 9 | Ghost система не интегрирована | `ghost/` | Подключить `GhostRecorder` к `GeneratorLifecycle` |
|
| 9 | Ghost система не интегрирована | `ghost/` | Подключить `GhostRecorder` к `GeneratorLifecycle` |
|
||||||
| 10 | Удалить мёртвые stub-файлы | `generator/`, корень | `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
|
| ✅ | ~~Удалить мёртвые stub-файлы~~ | `generator/`, корень | ✅ Удалены: `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
|
||||||
| 11 | ElytraMode: кольца из партиклов + детекция пролёта | `ElytraMode.java` | BoundingBox check в `tick()` |
|
| 11 | ElytraMode: кольца из партиклов + детекция пролёта | `ElytraMode.java` | BoundingBox check в `tick()` |
|
||||||
| 12 | ElytraMode: cooldown на фейерверки | `ElytraMode.java` | Счётчик + сообщение |
|
| 12 | ElytraMode: cooldown на фейерверки | `ElytraMode.java` | Счётчик + сообщение |
|
||||||
| 13 | RaceMode | новый файл | Кто первый наберёт N очков |
|
| 13 | RaceMode | новый файл | Кто первый наберёт N очков |
|
||||||
| 14 | CoopMode | новый файл | Общий счёт нескольких игроков |
|
| 14 | CoopMode | новый файл | Общий счёт нескольких игроков |
|
||||||
|
| ✅ | ~~Реорганизация `generator/` по подпакетам~~ | `generator/` | ✅ Создана структура: `jump/`, `lifecycle/`, `effect/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ ✅ Реорганизация generator/ — ГОТОВО
|
||||||
|
|
||||||
|
Новая структура по логическим группам:
|
||||||
|
|
||||||
|
```
|
||||||
|
generator/
|
||||||
|
├── ParkourGenerator.java ← главный класс (183 строки)
|
||||||
|
├── GeneratorState.java ← данные генератора
|
||||||
|
├── GeneratorOption.java ← enum опций
|
||||||
|
├── Profile.java ← профиль настроек
|
||||||
|
├── Island.java ← остров спавна
|
||||||
|
│
|
||||||
|
├── jump/ ← всё про прыжки и размещение блоков
|
||||||
|
│ ├── BlockPlacer.java (166 строк)
|
||||||
|
│ ├── JumpDirector.java
|
||||||
|
│ ├── JumpOffsetGenerator.java
|
||||||
|
│ ├── JumpValidator.java
|
||||||
|
│ └── JumpType.java
|
||||||
|
│
|
||||||
|
├── lifecycle/ ← жизненный цикл: тик, падение, очистка
|
||||||
|
│ └── GeneratorLifecycle.java
|
||||||
|
│
|
||||||
|
└── effect/ ← визуальные эффекты
|
||||||
|
└── EffectManager.java
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
- ✅ Все файлы перемещены через `smartRelocate` (импорты обновлены автоматически)
|
||||||
|
- ✅ Удалены мёртвые stub-файлы
|
||||||
|
- ✅ Проект компилируется без ошибок
|
||||||
|
- ✅ Структура стала логичнее и понятнее
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -55,12 +112,13 @@
|
||||||
- `LoParkourCommand` разбит на `PlayerCommandHandler` + `AdminCommandHandler` + `SchematicCommandHandler`
|
- `LoParkourCommand` разбит на `PlayerCommandHandler` + `AdminCommandHandler` + `SchematicCommandHandler`
|
||||||
- `Events` разбит на 3 listener'а
|
- `Events` разбит на 3 listener'а
|
||||||
- `StorageSQL` разбит на `SQLConnectionManager` + `SQLQueryExecutor` + `SQLMigrationManager`
|
- `StorageSQL` разбит на `SQLConnectionManager` + `SQLQueryExecutor` + `SQLMigrationManager`
|
||||||
|
- ✅ **Проект компилируется без ошибок** (исправлены все баги после рефакторинга)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎮 Чек-лист перед релизом
|
## 🎮 Чек-лист перед релизом
|
||||||
|
|
||||||
- [ ] `./gradlew build` без ошибок
|
- [x] `./gradlew build` без ошибок
|
||||||
- [ ] Запуск сервера без ошибок в консоли
|
- [ ] Запуск сервера без ошибок в консоли
|
||||||
- [ ] Паркур: прыжки, счёт, рекорд работают
|
- [ ] Паркур: прыжки, счёт, рекорд работают
|
||||||
- [ ] Scoreboard обновляется
|
- [ ] Scoreboard обновляется
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package dev.loki.loparkour;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Replaced by {@link LoParkourCommand}. Delete this file.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
class Command extends LoParkourCommand {}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package dev.loki.loparkour;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Split into:
|
|
||||||
* - {@link dev.loki.loparkour.listener.PlayerConnectionListener}
|
|
||||||
* - {@link dev.loki.loparkour.listener.ParkourRestrictionListener}
|
|
||||||
* - {@link dev.loki.loparkour.listener.SchematicWandListener}
|
|
||||||
*
|
|
||||||
* Delete this file after verifying compilation.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
class Events {}
|
|
||||||
|
|
@ -14,6 +14,7 @@ import dev.loki.loparkour.mode.Modes;
|
||||||
import dev.loki.loparkour.mode.SpectatorMode;
|
import dev.loki.loparkour.mode.SpectatorMode;
|
||||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
import dev.loki.loparkour.mode.SpeedrunMode;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.ParkourUser;
|
||||||
|
import dev.loki.loparkour.player.UserRegistry;
|
||||||
import dev.loki.loparkour.reward.Rewards;
|
import dev.loki.loparkour.reward.Rewards;
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
|
import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
|
||||||
import dev.loki.loparkour.storage.Storage;
|
import dev.loki.loparkour.storage.Storage;
|
||||||
|
|
@ -243,8 +244,7 @@ public final class LoParkour extends LoPlugin {
|
||||||
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
||||||
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.locales.size())));
|
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.locales.size())));
|
||||||
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
|
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
|
||||||
int joins = ParkourUser.joinCount;
|
int joins = UserRegistry.getJoinCount();
|
||||||
ParkourUser.joinCount = 0;
|
|
||||||
return joins;
|
return joins;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,12 @@ public enum Config {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the file so all keys are present.
|
* Updates the file so all keys are present.
|
||||||
|
* Any key that exists in the bundled default but is missing on disk
|
||||||
|
* will be appended to the correct section automatically.
|
||||||
*/
|
*/
|
||||||
public void update() {
|
public void update() {
|
||||||
try {
|
try {
|
||||||
// TODO: ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
|
ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
||||||
"Error while trying to update config file: " + fileName, ex);
|
"Error while trying to update config file: " + fileName, ex);
|
||||||
|
|
|
||||||
287
src/main/java/dev/loki/loparkour/config/ConfigUpdater.java
Normal file
287
src/main/java/dev/loki/loparkour/config/ConfigUpdater.java
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
package dev.loki.loparkour.config;
|
||||||
|
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically adds missing keys from the default (bundled) config
|
||||||
|
* into the player's config file on disk, preserving all existing values,
|
||||||
|
* comments, and blank lines.
|
||||||
|
*
|
||||||
|
* <p>Algorithm:
|
||||||
|
* <ol>
|
||||||
|
* <li>Load the default config from the jar as a list of raw lines.</li>
|
||||||
|
* <li>Load the on-disk config as a flat key→value map.</li>
|
||||||
|
* <li>Walk the default lines; for every key that is missing on disk,
|
||||||
|
* emit the default line (with its comment header) into the output.</li>
|
||||||
|
* <li>Rewrite the on-disk file with the merged result.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* Sections listed in {@code ignoredSections} are never touched — their
|
||||||
|
* default content is never injected (user manages them freely).
|
||||||
|
*/
|
||||||
|
public final class ConfigUpdater {
|
||||||
|
|
||||||
|
private ConfigUpdater() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates {@code diskFile} by injecting any keys that exist in the
|
||||||
|
* bundled default but are absent on disk.
|
||||||
|
*
|
||||||
|
* @param plugin the plugin (used to read the bundled resource)
|
||||||
|
* @param resourceName path inside the jar, e.g. {@code "config.yml"}
|
||||||
|
* @param diskFile the on-disk file to update
|
||||||
|
* @param ignoredSections top-level sections whose keys should never be added
|
||||||
|
* (pass {@code null} or empty list to ignore nothing)
|
||||||
|
*/
|
||||||
|
public static void update(
|
||||||
|
@NotNull Plugin plugin,
|
||||||
|
@NotNull String resourceName,
|
||||||
|
@NotNull File diskFile,
|
||||||
|
@Nullable List<String> ignoredSections
|
||||||
|
) throws IOException {
|
||||||
|
|
||||||
|
List<String> defaultLines = readResource(plugin, resourceName);
|
||||||
|
if (defaultLines == null) {
|
||||||
|
plugin.getLogger().warning("[ConfigUpdater] Resource not found in jar: " + resourceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat key set of what already exists on disk (dot-separated paths)
|
||||||
|
Set<String> diskKeys = flatKeys(diskFile);
|
||||||
|
|
||||||
|
// Ignored section prefixes (e.g. "styles" → skip any path starting with "styles.")
|
||||||
|
Set<String> ignored = normalizedIgnored(ignoredSections);
|
||||||
|
|
||||||
|
List<String> diskLines = readLines(diskFile);
|
||||||
|
List<String> outputLines = merge(defaultLines, diskLines, diskKeys, ignored);
|
||||||
|
|
||||||
|
// Only write if something actually changed
|
||||||
|
if (!outputLines.equals(diskLines)) {
|
||||||
|
writeLines(diskFile, outputLines);
|
||||||
|
plugin.getLogger().info("[ConfigUpdater] Updated " + resourceName
|
||||||
|
+ " — added " + countNew(diskLines, outputLines) + " new key(s).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Merge ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges default lines into disk lines.
|
||||||
|
*
|
||||||
|
* <p>Strategy: walk disk lines first, emit them as-is. Then walk default
|
||||||
|
* lines; for each key block (comment + key line) whose key is absent on
|
||||||
|
* disk and not ignored, append it at the correct indentation level.
|
||||||
|
*/
|
||||||
|
private static List<String> merge(
|
||||||
|
List<String> defaultLines,
|
||||||
|
List<String> diskLines,
|
||||||
|
Set<String> diskKeys,
|
||||||
|
Set<String> ignored
|
||||||
|
) {
|
||||||
|
// We need to insert missing keys at the right position.
|
||||||
|
// Build a copy of disk lines we will extend.
|
||||||
|
List<String> result = new ArrayList<>(diskLines);
|
||||||
|
|
||||||
|
// Collect blocks from the default file: each block = list of comment
|
||||||
|
// lines + the key line itself. We track the "path stack" to know the
|
||||||
|
// full dotted path of every key.
|
||||||
|
Deque<String> pathStack = new ArrayDeque<>();
|
||||||
|
List<String> pendingComments = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < defaultLines.size(); i++) {
|
||||||
|
String raw = defaultLines.get(i);
|
||||||
|
String trimmed = raw.stripLeading();
|
||||||
|
|
||||||
|
// Blank line → flush pending comments, keep as separator
|
||||||
|
if (trimmed.isBlank()) {
|
||||||
|
pendingComments.add(raw);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment line → accumulate
|
||||||
|
if (trimmed.startsWith("#")) {
|
||||||
|
pendingComments.add(raw);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key line
|
||||||
|
int indent = raw.length() - trimmed.length();
|
||||||
|
int depth = indent / 2; // assume 2-space indentation
|
||||||
|
|
||||||
|
// Pop stack back to current depth
|
||||||
|
while (pathStack.size() > depth) pathStack.pollLast();
|
||||||
|
|
||||||
|
// Parse key name (everything before ':')
|
||||||
|
String keyPart = trimmed.split(":")[0].trim();
|
||||||
|
pathStack.addLast(keyPart);
|
||||||
|
String fullPath = String.join(".", pathStack);
|
||||||
|
|
||||||
|
boolean isSection = trimmed.endsWith(":") || trimmed.matches(".*:\\s*$");
|
||||||
|
boolean isIgnored = isIgnored(fullPath, ignored);
|
||||||
|
|
||||||
|
if (!isIgnored && !diskKeys.contains(fullPath) && !isSection) {
|
||||||
|
// This key is missing on disk — inject it
|
||||||
|
// Find the best insertion point: after the last line that
|
||||||
|
// belongs to the parent section on disk
|
||||||
|
int insertAt = findInsertionPoint(result, pathStack, depth);
|
||||||
|
|
||||||
|
// Insert blank separator + comments + key line
|
||||||
|
List<String> block = new ArrayList<>();
|
||||||
|
if (insertAt == result.size() || !result.get(Math.max(0, insertAt - 1)).isBlank()) {
|
||||||
|
block.add("");
|
||||||
|
}
|
||||||
|
block.addAll(pendingComments);
|
||||||
|
block.add(raw);
|
||||||
|
result.addAll(insertAt, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pending comments regardless
|
||||||
|
pendingComments.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the line index where a missing key should be inserted.
|
||||||
|
* Inserts after the last line of its parent section.
|
||||||
|
*/
|
||||||
|
private static int findInsertionPoint(List<String> lines, Deque<String> pathStack, int depth) {
|
||||||
|
if (depth == 0) {
|
||||||
|
// Top-level key → append at end
|
||||||
|
return lines.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent path
|
||||||
|
List<String> parts = new ArrayList<>(pathStack);
|
||||||
|
String parentKey = parts.get(depth - 1);
|
||||||
|
int parentIndent = (depth - 1) * 2;
|
||||||
|
|
||||||
|
// Find the parent key line in the result
|
||||||
|
int parentLine = -1;
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
String l = lines.get(i);
|
||||||
|
int ind = l.length() - l.stripLeading().length();
|
||||||
|
if (ind == parentIndent && l.stripLeading().startsWith(parentKey + ":")) {
|
||||||
|
parentLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentLine < 0) return lines.size();
|
||||||
|
|
||||||
|
// Find end of parent section: next line with same or lower indent that isn't blank/comment
|
||||||
|
for (int i = parentLine + 1; i < lines.size(); i++) {
|
||||||
|
String l = lines.get(i);
|
||||||
|
if (l.isBlank() || l.stripLeading().startsWith("#")) continue;
|
||||||
|
int ind = l.length() - l.stripLeading().length();
|
||||||
|
if (ind <= parentIndent) return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── IO helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Reads a resource from the jar as a list of lines. Returns null if not found. */
|
||||||
|
@Nullable
|
||||||
|
private static List<String> readResource(Plugin plugin, String name) {
|
||||||
|
try (InputStream in = plugin.getResource(name)) {
|
||||||
|
if (in == null) return null;
|
||||||
|
return readLines(in);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Failed to read resource: " + name, ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> readLines(File file) throws IOException {
|
||||||
|
try (BufferedReader r = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
return readLines(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> readLines(InputStream in) throws IOException {
|
||||||
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||||
|
return readLines(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> readLines(BufferedReader reader) throws IOException {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) lines.add(line);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeLines(File file, List<String> lines) throws IOException {
|
||||||
|
try (BufferedWriter w = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
w.write(lines.get(i));
|
||||||
|
if (i < lines.size() - 1) w.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Key extraction ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the flat set of dotted-path keys present in the file.
|
||||||
|
* Only leaf keys (not sections) are returned.
|
||||||
|
*/
|
||||||
|
private static Set<String> flatKeys(File file) throws IOException {
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
Deque<String> stack = new ArrayDeque<>();
|
||||||
|
|
||||||
|
List<String> lines = readLines(file);
|
||||||
|
for (String raw : lines) {
|
||||||
|
String trimmed = raw.stripLeading();
|
||||||
|
if (trimmed.isBlank() || trimmed.startsWith("#")) continue;
|
||||||
|
|
||||||
|
int indent = raw.length() - trimmed.length();
|
||||||
|
int depth = indent / 2;
|
||||||
|
|
||||||
|
while (stack.size() > depth) stack.pollLast();
|
||||||
|
|
||||||
|
if (!trimmed.contains(":")) continue;
|
||||||
|
String keyPart = trimmed.split(":")[0].trim();
|
||||||
|
stack.addLast(keyPart);
|
||||||
|
|
||||||
|
boolean isSection = trimmed.matches("[^:]+:\\s*$");
|
||||||
|
if (!isSection) {
|
||||||
|
keys.add(String.join(".", stack));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static Set<String> normalizedIgnored(@Nullable List<String> list) {
|
||||||
|
if (list == null || list.isEmpty()) return Collections.emptySet();
|
||||||
|
Set<String> result = new LinkedHashSet<>();
|
||||||
|
for (String s : list) result.add(s.toLowerCase(Locale.ROOT).trim());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isIgnored(String fullPath, Set<String> ignored) {
|
||||||
|
String lower = fullPath.toLowerCase(Locale.ROOT);
|
||||||
|
for (String prefix : ignored) {
|
||||||
|
if (lower.equals(prefix) || lower.startsWith(prefix + ".")) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countNew(List<String> before, List<String> after) {
|
||||||
|
return Math.max(0, after.size() - before.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package dev.loki.loparkour.generator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Logic merged into {@link BlockPlacer}. Delete this file.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
class BlockSelector {}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package dev.loki.loparkour.generator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Tick logic was merged back into {@link ParkourGenerator#tick()}.
|
|
||||||
* Delete this file.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
class GeneratorTick {}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package dev.loki.loparkour.generator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Score logic lives in {@link GeneratorLifecycle}.
|
|
||||||
* This class was created but never wired in. Delete this file.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
class ScoreManager {}
|
|
||||||
|
|
@ -54,12 +54,11 @@ public class EffectManager {
|
||||||
);
|
);
|
||||||
viewers.forEach(p -> p.player.spawnParticle(
|
viewers.forEach(p -> p.player.spawnParticle(
|
||||||
Option.PARTICLE_TYPE, center, 5, 0.1, 0.1, 0.1, 0,
|
Option.PARTICLE_TYPE, center, 5, 0.1, 0.1, 0.1, 0,
|
||||||
Option.PARTICLE_DATA != null ? Option.PARTICLE_DATA.data() : null
|
Option.PARTICLE_DATA
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
case CIRCLE -> ParticleUtil.circle(
|
case CIRCLE -> ParticleUtil.circle(
|
||||||
min.clone().add(0.5, 0.5, 0.5), max.getWorld(), Option.PARTICLE_TYPE,
|
min.clone().add(0.5, 0.5, 0.5), Option.PARTICLE_TYPE, 8, 1
|
||||||
viewers.stream().map(p -> p.player).toList(), 0.5, 8
|
|
||||||
);
|
);
|
||||||
case BOX -> ParticleUtil.box(
|
case BOX -> ParticleUtil.box(
|
||||||
org.bukkit.util.BoundingBox.of(min, max), max.getWorld(), Option.PARTICLE_TYPE,
|
org.bukkit.util.BoundingBox.of(min, max), max.getWorld(), Option.PARTICLE_TYPE,
|
||||||
|
|
@ -106,7 +106,7 @@ public class PAPIHook extends PlaceholderExpansion {
|
||||||
ParkourGenerator generator = pp.session.generator;
|
ParkourGenerator generator = pp.session.generator;
|
||||||
switch (params) {
|
switch (params) {
|
||||||
case "score", "current_score" -> {
|
case "score", "current_score" -> {
|
||||||
return Integer.toString(generator.score);
|
return Integer.toString(generator.state.score);
|
||||||
}
|
}
|
||||||
case "time", "current_time" -> {
|
case "time", "current_time" -> {
|
||||||
return generator.getFormattedTime();
|
return generator.getFormattedTime();
|
||||||
|
|
@ -134,7 +134,7 @@ public class PAPIHook extends PlaceholderExpansion {
|
||||||
String replaced = params.replace("score_until_", "");
|
String replaced = params.replace("score_until_", "");
|
||||||
int interval = Integer.parseInt(replaced);
|
int interval = Integer.parseInt(replaced);
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
return Integer.toString(interval - (generator.totalScore % interval)); // 100 - (5 % 100) = 95
|
return Integer.toString(interval - (generator.state.score % interval));
|
||||||
} else {
|
} else {
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
||||||
open(pp);
|
open(pp);
|
||||||
})
|
})
|
||||||
.setItem(12, toggleItem(locale, "special_blocks", pp.useSpecialBlocks), e -> {
|
.setItem(12, toggleItem(locale, "special_blocks", pp.useSpecialBlocks), e -> {
|
||||||
if (pp.session.generator.score == 0) {
|
if (pp.session.generator.state.score == 0) {
|
||||||
pp.useSpecialBlocks = !pp.useSpecialBlocks;
|
pp.useSpecialBlocks = !pp.useSpecialBlocks;
|
||||||
pp.updateGeneratorSettings(pp.session.generator);
|
pp.updateGeneratorSettings(pp.session.generator);
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
||||||
.setItem(19, localeItem(player, "settings.parkour_settings.styles.item"),
|
.setItem(19, localeItem(player, "settings.parkour_settings.styles.item"),
|
||||||
e -> openStyleMenu(pp))
|
e -> openStyleMenu(pp))
|
||||||
.setItem(20, schemDiffItem(pp), e -> {
|
.setItem(20, schemDiffItem(pp), e -> {
|
||||||
if (pp.session.generator.score == 0) {
|
if (pp.session.generator.state.score == 0) {
|
||||||
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
|
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
|
||||||
int idx = diffs.indexOf(pp.schematicDifficulty);
|
int idx = diffs.indexOf(pp.schematicDifficulty);
|
||||||
pp.schematicDifficulty = diffs.get((idx + 1) % diffs.size());
|
pp.schematicDifficulty = diffs.get((idx + 1) % diffs.size());
|
||||||
|
|
|
||||||
20
src/main/java/dev/loki/loparkour/mode/ElytraGenerator.java
Normal file
20
src/main/java/dev/loki/loparkour/mode/ElytraGenerator.java
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package dev.loki.loparkour.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.session.Session;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator for Elytra mode parkour
|
||||||
|
*/
|
||||||
|
public class ElytraGenerator extends ParkourGenerator {
|
||||||
|
|
||||||
|
public ElytraGenerator(@NotNull Session session) {
|
||||||
|
super(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mode getMode() {
|
||||||
|
return Modes.ELYTRA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,7 +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 = PlayerSettingsManager.getColumnMappings();
|
public static final Map<String, PlayerSettingsManager.OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
|
||||||
|
|
||||||
public @Expose Double schematicDifficulty;
|
public @Expose Double schematicDifficulty;
|
||||||
public @Expose Integer blockLead;
|
public @Expose Integer blockLead;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import java.util.function.BiConsumer;
|
||||||
*
|
*
|
||||||
* @since 5.0.0
|
* @since 5.0.0
|
||||||
*/
|
*/
|
||||||
class PlayerSettingsManager {
|
public class PlayerSettingsManager {
|
||||||
|
|
||||||
private static final Map<String, OptionContainer> COLUMN_MAPPINGS = new HashMap<>();
|
private static final Map<String, OptionContainer> COLUMN_MAPPINGS = new HashMap<>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,9 @@ public class UserRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getJoinCount() {
|
public static int getJoinCount() {
|
||||||
return joinCount;
|
int count = joinCount;
|
||||||
|
joinCount = 0;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void resetScoreboard(ParkourUser user) {
|
private static void resetScoreboard(ParkourUser user) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue